"""Neo Cortex MCP — dual mode memory tools.

Modes (controlled by ENV vars):
    - Proxy (default): forwards to neo-cortex HTTP API on port 5074
    - Embedded: CORTEX_DATA_DIR set → local CortexService + ChromaDB
    - Embedded + subscriber: CORTEX_DATA_DIR + MEMORY_HUB_URL set
      → also runs SignalR subscriber in daemon thread

Usage:
    python -m neo_cortex.mcp          # stdio transport (for Claude Code)
    python -m neo_cortex.mcp --http   # HTTP transport (for testing)
"""

# ============================================================
# BOOT LOGGING — raw file I/O before ANY third-party imports
# ============================================================
import os
import sys
import time

try:
    from importlib.metadata import version as _pkg_version
    __version__ = _pkg_version("neo-cortex-mcp")
except Exception:
    __version__ = "5.1.5"

_data_dir = os.environ.get("CORTEX_DATA_DIR")
_hub_url = os.environ.get("MEMORY_HUB_URL")
_embedded = _data_dir is not None

_log_dir = os.path.join(_data_dir, "cortex_db") if _data_dir else os.getcwd()
_log_path = os.path.join(_log_dir, "neo-cortex.log")
os.makedirs(_log_dir, exist_ok=True)


def _bl(msg: str) -> None:
    """Boot log — write directly to file before logging module is configured."""
    line = f"{time.strftime('%Y-%m-%d %H:%M:%S')} [BOOT] {msg}\n"
    try:
        with open(_log_path, "a", encoding="utf-8") as f:
            f.write(line)
    except Exception:
        pass


_bl(f"{'='*60}")
_bl(f"neo-cortex-mcp v{__version__} STARTING")
_bl(f"python: {sys.version}")
_bl(f"executable: {sys.executable}")
_bl(f"platform: {sys.platform}")
_bl(f"pid: {os.getpid()}")
_bl(f"cwd: {os.getcwd()}")
_bl(f"CORTEX_DATA_DIR: {_data_dir}")
_bl(f"MEMORY_HUB_URL: {_hub_url}")
_bl(f"JINA_API_KEY: {'SET' if os.environ.get('JINA_API_KEY') else 'NOT SET'}")
_bl(f"CLASSIFIER_PROVIDER: {os.environ.get('CLASSIFIER_PROVIDER', 'gemini')}")
_bl(f"embedded mode: {_embedded}")

# ============================================================
# OPENSSL FIX — must run BEFORE any ssl/httpx imports
# uv-managed Python on Windows has broken OpenSSL (OPENSSL_Applink crash).
# Root cause: SSLKEYLOGFILE env var (set by security software) + DLL mismatch.
# See: github.com/astral-sh/python-build-standalone/issues/640
# See: github.com/astral-sh/uv/issues/14333
# ============================================================
if sys.platform == "win32":
    _had_keylog = "SSLKEYLOGFILE" in os.environ
    if _had_keylog:
        del os.environ["SSLKEYLOGFILE"]
        _bl(f"OPENSSL FIX: removed SSLKEYLOGFILE env var")
    else:
        _bl("OPENSSL FIX: SSLKEYLOGFILE not set (good)")

    try:
        import ssl as _ssl_mod
        import _ssl
        _openssl_ver = _ssl_mod.OPENSSL_VERSION
        _bl(f"OPENSSL FIX: ssl force-init OK → {_openssl_ver}")
    except Exception as _ssl_err:
        _bl(f"OPENSSL FIX: ssl force-init FAILED: {_ssl_err}")
else:
    _bl("OPENSSL FIX: not Windows, skipping")

# ============================================================
# IMPORTS — logged one by one to find crash point
# ============================================================
_bl("import: logging")
import logging

_bl("import: threading")
import threading

_bl("import: pathlib.Path")
from pathlib import Path

_bl("import: typing.Optional")
from typing import Optional

_bl("import: httpx")
try:
    import httpx
    _bl("import: httpx OK")
except Exception as e:
    _bl(f"import: httpx FAILED: {e}")
    raise

_bl("import: fastmcp.FastMCP")
try:
    from fastmcp import FastMCP
    _bl("import: fastmcp OK")
except Exception as e:
    _bl(f"import: fastmcp FAILED: {e}")
    raise

# ============================================================
# LOGGER setup (now that logging is imported)
# ============================================================
logger = logging.getLogger("neo-cortex-mcp")

# ============================================================
# MODE INIT — embedded or proxy
# ============================================================
_cortex = None
_digestor = None
_init_error = None

if _embedded:
    _bl("embedded: importing subscriber.configure_paths")
    from neo_cortex.subscriber import configure_paths
    _bl("embedded: configure_paths()")
    configure_paths(_data_dir)
    Path(_data_dir).mkdir(parents=True, exist_ok=True)

    try:
        _bl("embedded: importing neo_cortex.config")
        from neo_cortex import config
        _bl(f"embedded: CORTEX_DB_PATH={config.CORTEX_DB_PATH}")
        _bl(f"embedded: MEMORY_INDEX_DB_PATH={config.MEMORY_INDEX_DB_PATH}")
        _bl(f"embedded: CONVERSATION_DB_PATH={config.CONVERSATION_DB_PATH}")

        _bl("embedded: importing Classifier + LLM")
        from neo_cortex.classifier import Classifier
        from neo_cortex.llm import create_client

        _bl("embedded: importing CortexService")
        from neo_cortex.cortex import CortexService

        _bl("embedded: importing JinaEmbedder")
        from neo_cortex.embedder import JinaEmbedder

        _bl("embedded: importing MemoryIndex")
        from neo_cortex.memory_index import MemoryIndex

        _bl("embedded: importing MemoryStore")
        from neo_cortex.store import MemoryStore

        _bl("embedded: importing v5 components")
        from neo_cortex.conversation_log import ConversationLog
        from neo_cortex.dedup import DedupStore
        from neo_cortex.digestor import EpisodeDigestor
        from neo_cortex.graph import ConceptGraph

        _bl(f"embedded: creating MemoryStore(db_path={config.CORTEX_DB_PATH})")
        store = MemoryStore(config.CORTEX_DB_PATH, config.COLLECTION_NAME)
        _bl(f"embedded: MemoryStore OK, count={store.count()}")

        _bl("embedded: load_api_key('jina')")
        jina_key = config.load_api_key("jina")
        _bl(f"embedded: jina key loaded ({len(jina_key)} chars)")

        _bl(f"embedded: platform={sys.platform}, verify_ssl={sys.platform != 'win32'}")

        _bl("embedded: creating JinaEmbedder")
        embedder = JinaEmbedder(jina_key)
        _bl("embedded: JinaEmbedder OK")

        _bl(f"embedded: creating LLM client (provider={config.CLASSIFIER_PROVIDER})")
        llm_client = create_client()
        _bl(f"embedded: LLM client OK → {llm_client.name}")

        _bl("embedded: creating Classifier")
        classifier = Classifier(llm_client)
        _bl("embedded: Classifier OK")

        _bl("embedded: creating MemoryIndex")
        index = MemoryIndex(config.MEMORY_INDEX_DB_PATH)
        _bl(f"embedded: MemoryIndex OK, count={index.count()}")

        # v5 components — dedup + graph share memory_index connection
        _bl("embedded: creating DedupStore (shared conn)")
        dedup = DedupStore(index.conn)
        _bl("embedded: DedupStore OK")

        _bl("embedded: creating ConceptGraph (shared conn)")
        graph = ConceptGraph(index.conn)
        _bl(f"embedded: ConceptGraph OK, nodes={graph.node_count()}")

        _bl("embedded: creating CortexService (v5: dedup+graph)")
        _cortex = CortexService(store, embedder, classifier, index=index, dedup=dedup, graph=graph)
        _bl("embedded: CortexService v5 READY")

        # Consolidator (phase 2 — merge similar memories during idle)
        from neo_cortex.merge_consolidator import MemoryConsolidator
        _bl("embedded: creating MemoryConsolidator")
        _consolidator = MemoryConsolidator(
            store=store, index=index, embedder=embedder,
            llm=llm_client, dedup=dedup, graph=graph,
        )
        _bl("embedded: MemoryConsolidator READY")

        # Distiller (Gemini — for digestor distill mode)
        distiller = None
        if config.DIGESTOR_MODE == "distill":
            try:
                from neo_cortex.distiller import GeminiDistiller
                _bl("embedded: load_api_key('gemini')")
                gemini_key = config.load_api_key("gemini")
                _bl(f"embedded: gemini key loaded ({len(gemini_key)} chars)")
                distiller = GeminiDistiller(gemini_key)
                _bl(f"embedded: GeminiDistiller OK (model={config.GEMINI_MODEL})")
            except KeyError:
                _bl("embedded: no gemini key found, falling back to segment mode")
            except Exception as e:
                _bl(f"embedded: GeminiDistiller FAILED: {e}, falling back to segment mode")

        # Digestor
        _bl(f"embedded: creating ConversationLog({config.CONVERSATION_DB_PATH})")
        _conv_log = ConversationLog(config.CONVERSATION_DB_PATH)
        _conv_count = _conv_log.count()
        _bl(f"embedded: ConversationLog OK, entries={_conv_count}")

        _mode = "distill" if distiller else "segment"
        _bl(f"embedded: creating EpisodeDigestor (mode={_mode})")
        _digestor = EpisodeDigestor(log=_conv_log, cortex=_cortex, classifier=classifier, distiller=distiller, consolidator=_consolidator)
        _bl("embedded: EpisodeDigestor READY — will start background thread in main()")

    except Exception as e:
        _init_error = str(e)
        _bl(f"embedded: INIT FAILED: {e}")
        import traceback
        _bl(traceback.format_exc())
        _cortex = None
else:
    _bl("standalone: no CORTEX_DATA_DIR set — tools will return errors")


def _check_embedded():
    """Raise clear error if embedded mode but cortex init failed."""
    if _embedded and _cortex is None:
        raise RuntimeError(f"CortexService init failed: {_init_error}. Check JINA_API_KEY and GEMINI_API_KEY env vars.")


# --- Subscriber thread (embedded + hub_url) ---
def _start_subscriber(hub_url: str, cortex) -> threading.Thread:
    """Start SignalR subscriber in a daemon thread with its own asyncio loop.

    Feeds BOTH paths:
      - Strada 1: cortex.ingest() per turn (noise filter)
      - Strada 2: conversation_log.append() per event (digestor, no noise filter)
    """
    import asyncio as _asyncio

    from neo_cortex.models import ConversationAppendRequest
    from neo_cortex.subscriber import TurnAccumulator

    async def _run():
        from pysignalr.client import SignalRClient

        client = SignalRClient(hub_url)
        accumulator = TurnAccumulator()

        def _append_log(session_id: str, role: str, content: str) -> None:
            """Feed conversation_log (Strada 2) — safe, never throws."""
            if not _conv_log or not content:
                return
            try:
                _conv_log.append(ConversationAppendRequest(
                    session_id=session_id, role=role, content=content,
                ))
            except Exception as e:
                _bl(f"SUBSCRIBER: conv_log append failed: {e}")

        async def on_user_message(args):
            data = args[0] if args else {}
            sid = data.get("sessionId", "")
            text = data.get("text", "")
            accumulator.on_user_message(sid, text)
            _append_log(sid, "user", text)

        async def on_assistant_message(args):
            data = args[0] if args else {}
            sid = data.get("sessionId", "")
            text = data.get("text", "")
            accumulator.on_assistant_message(sid, text, data.get("model", "unknown"))
            _append_log(sid, "assistant", text)

        async def on_tool_use(args):
            data = args[0] if args else {}
            sid = data.get("sessionId", "")
            tool_name = data.get("toolName", "")
            accumulator.on_tool_use(sid, tool_name)
            _append_log(sid, "tool_use", tool_name)

        async def on_tool_result(args):
            data = args[0] if args else {}
            sid = data.get("sessionId", "")
            output = data.get("output", "")
            # Truncate large tool results for conversation_log
            if len(output) > 2000:
                output = output[:2000] + "\n[truncated]"
            _append_log(sid, "tool_result", output)

        async def on_turn_complete(args):
            if not accumulator.is_complete:
                accumulator.reset()
                return
            from neo_cortex.models import IngestRequest
            req = IngestRequest(
                session_id=accumulator.session_id,
                question=accumulator.question,
                answer=accumulator.answer,
                model=accumulator.model,
                tools_used=accumulator.tools_used,
                turn_number=accumulator.turn_number,
            )
            try:
                memory_id = await cortex.ingest(req)
                if memory_id:
                    logger.info("Subscriber ingested: %s (turn %d)", memory_id, accumulator.turn_number)
            except Exception:
                logger.exception("Subscriber ingest failed for turn %d", accumulator.turn_number)
            accumulator.reset()

        client.on("MemoryUserMessage", on_user_message)
        client.on("MemoryAssistantMessage", on_assistant_message)
        client.on("MemoryToolUse", on_tool_use)
        client.on("MemoryToolResult", on_tool_result)
        client.on("MemoryTurnComplete", on_turn_complete)

        _bl(f"SUBSCRIBER: connecting to {hub_url}")
        await client.run()

    def _thread_target():
        try:
            _asyncio.run(_run())
        except Exception as e:
            # CRITICAL: catch ALL exceptions in subscriber thread.
            # An unhandled exception here can crash the entire process.
            _bl(f"SUBSCRIBER: CRASHED: {e}")
            import traceback
            _bl(f"SUBSCRIBER: {traceback.format_exc()}")

    t = threading.Thread(target=_thread_target, daemon=True, name="cortex-subscriber")
    t.start()
    _bl("SUBSCRIBER: thread started")
    return t


# --- MCP server ---
_bl("creating FastMCP server")
mcp = FastMCP(
    "neo-cortex",
    instructions="Neo's memory cortex. Semantic memory search, ingestion, dream cycles, and timeline.",
)
_bl("FastMCP server created")

# ============================================================
# AUTO-BOOTSTRAP — install hooks into .claude/settings.json
# ============================================================
try:
    from neo_cortex.bootstrap import ensure_hooks
    _boot_res = ensure_hooks()
    if _boot_res.error:
        _bl(f"bootstrap: skipped — {_boot_res.error}")
    elif _boot_res.already_present:
        _bl("bootstrap: hooks already installed")
    else:
        _parts = []
        if _boot_res.session_start_installed:
            _parts.append("SessionStart")
        if _boot_res.stop_installed:
            _parts.append("Stop")
        if _boot_res.created_dir:
            _parts.append("created .claude/")
        if _boot_res.created_file:
            _parts.append("created settings.json")
        _bl(f"bootstrap: installed — {', '.join(_parts)}")
except Exception as _boot_err:
    _bl(f"bootstrap: FAILED (non-fatal): {_boot_err}")


@mcp.tool()
async def memory_query(query: str, n: int = 5, detail: str = "compact") -> dict | str:
    """Search past conversation memories by semantic similarity.

    Args:
        query: What to search for (natural language)
        n: Number of results to return (default 5)
        detail: "compact" (MBEL + refs) or "full" (raw JSON for debug)
    """
    _bl(f"TOOL memory_query: query={query!r}, n={n}, detail={detail}")
    try:
        if _cortex:
            result = await _cortex.recall(query, n=n, smart=True)
            _bl("TOOL memory_query: OK")
            if detail == "full":
                return result.model_dump()
            # Default: pure MBEL with refs appended
            mbel = result.mbel or "(no results)"
            # Append refs as MBEL
            ref_lines = []
            for m in result.memories:
                title = m.record.topic or m.record.question[:80]
                if _cortex.index:
                    sf = _cortex.index.get_structured(m.record.id)
                    if sf and sf.title:
                        title = sf.title
                ref_lines.append(f"  {m.record.id}::{title}{{sim={m.similarity:.2f}}}")
            if ref_lines:
                mbel += "\n§refs\n" + "\n".join(ref_lines)
            return mbel
        return {"error": "no cortex available"}
    except Exception as e:
        _bl(f"TOOL memory_query: FAILED: {e}")
        import traceback
        _bl(traceback.format_exc())
        return {"error": str(e)}


@mcp.tool()
async def memory_stats(detail: str = "compact") -> dict:
    """Get cortex statistics: total memories, sessions, energy levels, dream count."""
    _bl("TOOL memory_stats")
    try:
        if _cortex:
            if detail == "full":
                result = _cortex.stats()
                _bl("TOOL memory_stats: OK (full)")
                return result.model_dump()
            # Compact: single SQL query via index
            if _cortex.index:
                _bl("TOOL memory_stats: OK (index)")
                return _cortex.index.stats(_cortex.dream_count)
            result = _cortex.stats()
            _bl("TOOL memory_stats: OK (store fallback)")
            return result.model_dump()
        return {"error": "no cortex available"}
    except Exception as e:
        _bl(f"TOOL memory_stats: FAILED: {e}")
        return {"error": str(e)}


@mcp.tool()
async def memory_dream(detail: str = "compact") -> dict:
    """Run one dream cycle: boost high-energy memories, decay others."""
    _bl("TOOL memory_dream")
    try:
        if _cortex:
            result = await _cortex.dream()
            _bl("TOOL memory_dream: OK")
            if detail == "full":
                return result.model_dump()
            return {
                "status": result.status,
                "dream_count": result.dream_count,
                "high_energy_count": len(result.cluster),
            }
        return {"error": "no cortex available"}
    except Exception as e:
        _bl(f"TOOL memory_dream: FAILED: {e}")
        return {"error": str(e)}


@mcp.tool()
async def memory_ingest(turn: dict) -> dict:
    """Manually ingest a conversation turn into memory.

    Args:
        turn: JSON with session_id, question, answer, model, stats
    """
    _bl(f"TOOL memory_ingest: keys={list(turn.keys())}")
    try:
        if _cortex:
            from neo_cortex.models import IngestRequest
            _bl("TOOL memory_ingest: creating IngestRequest")
            req = IngestRequest(**turn)
            _bl("TOOL memory_ingest: calling cortex.ingest")
            memory_id = await _cortex.ingest(req)
            _bl(f"TOOL memory_ingest: OK → {memory_id}")
            return {"status": "ok", "id": memory_id}
        return _post("/api/ingest", data=turn)
    except Exception as e:
        _bl(f"TOOL memory_ingest: FAILED: {e}")
        import traceback
        _bl(traceback.format_exc())
        return {"error": str(e)}


@mcp.tool()
async def memory_timeline(n: int = 10, project: Optional[str] = None, detail: str = "compact") -> dict | str:
    """Get most recent memories by timestamp (newest first).

    Use this at session start to load recent context, or to see what happened lately.

    Args:
        n: Number of recent memories to return (default 10)
        project: Optional filter by project name
        detail: "compact" (titles only) or "full" (raw JSON for debug)
    """
    _bl(f"TOOL memory_timeline: n={n}, project={project}, detail={detail}")
    try:
        if _cortex:
            if detail == "full":
                result = await _cortex.timeline(n=n, project=project)
                _bl("TOOL memory_timeline: OK (full)")
                return result.model_dump()
            # Default: pure MBEL
            result = await _cortex.timeline(n=n, project=project, smart=True)
            _bl(f"TOOL memory_timeline: OK (smart), {result.count} results")
            return result.mbel or "(no memories)"
        return {"error": "no cortex available"}
    except Exception as e:
        _bl(f"TOOL memory_timeline: FAILED: {e}")
        return {"error": str(e)}


@mcp.tool()
async def memory_search(query: str = "", project: Optional[str] = None,
                        activity: Optional[str] = None, n: int = 10) -> dict:
    """Search memory index. Returns compact results (title/project/date).
    Use memory_get for full details of specific memories.

    Args:
        query: Search text (or empty for recent)
        project: Filter by project name
        activity: Filter by activity type
        n: Max results
    """
    _bl(f"TOOL memory_search: query={query!r}")
    try:
        if _cortex:
            if _cortex.index:
                results = _cortex.index.search_fts(query=query, project=project, activity=activity, n=n)
                _bl(f"TOOL memory_search: OK, {len(results)} results")
                return {"results": [r.model_dump() for r in results]}
            result = await _cortex.recall(query or "recent", n=n)
            return result.model_dump()
        return {"error": "no cortex available"}
    except Exception as e:
        _bl(f"TOOL memory_search: FAILED: {e}")
        return {"error": str(e)}


@mcp.tool()
async def memory_get(ids: str, detail: str = "compact") -> dict:
    """Get full memory details by IDs (comma-separated).

    Args:
        ids: Comma-separated memory IDs (e.g. "v3_abc_1_12345,v3_def_2_67890")
        detail: "compact" (structured fields) or "full" (raw record for debug)
    """
    _bl(f"TOOL memory_get: ids={ids!r}, detail={detail}")
    try:
        if _cortex:
            if not _cortex.index:
                return {"error": "memory_get requires SQLite index in embedded mode"}
            id_list = [i.strip() for i in ids.split(",")]
            if detail == "full":
                records = _cortex.index.get_by_ids(id_list)
                _bl(f"TOOL memory_get: OK (full), {len(records)} records")
                return {"memories": [r.model_dump() for r in records]}
            # Compact: structured fields only, no document blob
            results = []
            for mid in id_list:
                sf = _cortex.index.get_structured(mid)
                if sf:
                    row = _cortex.index.get_by_id(mid)
                    results.append({
                        "id": mid,
                        "title": sf.title,
                        "summary": sf.summary,
                        "facts": sf.facts,
                        "concepts": sf.concepts,
                        "files_touched": sf.files_touched,
                        "project": row.project if row else "general",
                        "activity": row.activity.value if row and hasattr(row.activity, "value") else "discussion",
                        "energy": row.energy if row else 1.0,
                    })
            _bl(f"TOOL memory_get: OK (compact), {len(results)} results")
            return {"memories": results}
        return {"error": "no cortex available"}
    except Exception as e:
        _bl(f"TOOL memory_get: FAILED: {e}")
        return {"error": str(e)}


@mcp.tool()
async def memory_rebuild() -> dict:
    """Wipe all memories and rebuild from conversation log.

    Clears: ChromaDB, memory_index, dedup hashes, concept graph.
    Keeps: conversation_log (all turns marked as undigested).
    The digestor will automatically re-process everything.
    """
    _bl("TOOL memory_rebuild: START")
    try:
        if not _cortex:
            return {"error": "no cortex available"}

        # 1. Pause digestor
        if _digestor:
            _digestor.pause()
            _bl("TOOL memory_rebuild: digestor paused")

        # 2. Clear all memory stores
        cleared = {}

        cleared["chromadb"] = _cortex._store.clear()
        _bl(f"TOOL memory_rebuild: ChromaDB cleared ({cleared['chromadb']})")

        if _cortex._index:
            cleared["memory_index"] = _cortex._index.clear()
            _bl(f"TOOL memory_rebuild: memory_index cleared ({cleared['memory_index']})")

        if _cortex._dedup:
            cleared["dedup"] = _cortex._dedup.clear()
            _bl(f"TOOL memory_rebuild: dedup cleared ({cleared['dedup']})")

        if _cortex._graph:
            cleared["concept_graph"] = _cortex._graph.clear()
            _bl(f"TOOL memory_rebuild: concept_graph cleared ({cleared['concept_graph']})")

        if _cortex._index:
            cleared["pending_merges"] = _cortex._index.clear_pending_merges()
            _bl(f"TOOL memory_rebuild: pending_merges cleared ({cleared['pending_merges']})")

        # 3. Reset conversation log — mark all as undigested
        if _conv_log:
            cleared["turns_reset"] = _conv_log.reset_digested()
            cleared["total_turns"] = _conv_log.count()
            _bl(f"TOOL memory_rebuild: conversation_log reset ({cleared['turns_reset']} turns)")

        # 4. Reset dream counter
        _cortex._dream_count = 0

        # 5. Resume digestor
        if _digestor:
            _digestor.resume()
            _bl("TOOL memory_rebuild: digestor resumed")

        _bl("TOOL memory_rebuild: DONE")
        return {"status": "rebuilt", "cleared": cleared}
    except Exception as e:
        _bl(f"TOOL memory_rebuild: FAILED: {e}")
        import traceback
        _bl(traceback.format_exc())
        # Resume digestor even on error
        if _digestor:
            _digestor.resume()
        return {"error": str(e)}


# --- Entrypoint ---


def _start_digestor(digestor) -> threading.Thread:
    """Start EpisodeDigestor in a daemon thread.

    Each tick: grab undigested turns, segment by topic, ingest complete segments.
    Sleeps 30s between ticks.
    """
    import asyncio as _asyncio

    async def _run():
        await digestor.run()

    def _thread_target():
        try:
            _asyncio.run(_run())
        except Exception as e:
            _bl(f"DIGESTOR: CRASHED: {e}")
            import traceback
            _bl(f"DIGESTOR: {traceback.format_exc()}")

    t = threading.Thread(target=_thread_target, daemon=True, name="cortex-digestor")
    t.start()
    _bl("DIGESTOR: thread started")
    return t


def main():
    _bl("main() called")

    # Configure structured logging (FileHandler + StreamHandler)
    logging.basicConfig(
        level=logging.INFO,
        format="%(asctime)s [%(name)s] %(levelname)s: %(message)s",
        handlers=[
            logging.FileHandler(_log_path, encoding="utf-8"),
            logging.StreamHandler(),
        ],
    )

    # Diagnostic summary
    import platform
    try:
        import chromadb as _chromadb
        chroma_ver = _chromadb.__version__
    except Exception:
        chroma_ver = "N/A"

    logger.info(
        "neo-cortex-mcp v%s | python %s | chromadb %s | os=%s | embedded=%s | data_dir=%s",
        __version__, platform.python_version(), chroma_ver,
        platform.system(), _embedded, _data_dir,
    )
    if _cortex:
        logger.info("CortexService v5 OK (dedup+graph+digestor)")
    else:
        logger.error("CortexService FAILED: %s", _init_error)

    # Start subscriber if both env vars set
    if _hub_url and _cortex:
        _start_subscriber(_hub_url, _cortex)

    # Start digestor background thread
    if _digestor and _cortex:
        _start_digestor(_digestor)

    _bl("starting MCP transport")
    if "--http" in sys.argv:
        mcp.run(transport="streamable-http", host="127.0.0.1", port=5075)
    else:
        mcp.run(transport="stdio")


if __name__ == "__main__":
    main()
