"""The core process — one command, three faces: the local UI, the MCP wire, the identity.

    python3 -m neoedge.serve --demo                       # a testbed edge with one dispute
    python3 -m neoedge.serve --book /data/book.jsonl --data /data

This is what runs inside the `edge` container of the compose topology, and it runs
identically on bare metal — the topology is a fact about processes and one TCP wire, not
about Docker. Faces and their audiences:

  * **UI** (`--ui-port`, default 8377) — the human's port. In compose it is published to
    the host's 127.0.0.1 and nowhere else.
  * **MCP over TCP** (`--mcp-port`, default 9137) — the agents' port. In compose it
    lives on an internal network; it is never published. The same withheld discipline
    guards both faces because both ARE the same surface (`EdgeMCPServer`).
  * **Identity** (`--data`) — the device key, created on first run, 0600, custody here
    and nowhere else. Its public half is shown; its private half has no port.

Bind hosts come from `NEOEDGE_UI_HOST` / `NEOEDGE_MCP_HOST` (default 127.0.0.1; the
container sets 0.0.0.0 inside its own namespace and compose decides what, if anything,
reaches the host).
"""

from __future__ import annotations

import argparse
import os
import signal
import sys
import threading
from http.server import ThreadingHTTPServer

from .identity import load_or_create
from .mcp.server import EdgeMCPServer
from .mcp.tcp import EdgeTCPServer
from .ui import make_handler


def main(argv: list[str] | None = None) -> int:
    ap = argparse.ArgumentParser(prog="python3 -m neoedge.serve")
    ap.add_argument("--demo", action="store_true", help="a testbed edge with one dispute")
    ap.add_argument("--book", help="serve a live book file, read-only")
    ap.add_argument("--ui-port", type=int, default=8377)
    ap.add_argument("--mcp-port", type=int, default=9137)
    ap.add_argument("--data", help="data directory (device identity lives here)")
    args = ap.parse_args(argv)

    counterparty = None
    if args.book:
        from .mcp.stdio import _load_edge

        edge = _load_edge(args.book)
    elif args.demo:
        from .mcp.stdio import _demo_edge

        edge, counterparty = _demo_edge()
    else:
        print("need --demo or --book", file=sys.stderr)
        return 2

    identity = None
    if args.data:
        identity = load_or_create(args.data)
        if identity is None:
            print("serve: no `cryptography` in this environment — running WITHOUT a device "
                  "identity (valid, and said out loud rather than faked)", file=sys.stderr)

    market_stop = threading.Event()
    market_thread = None
    market_config = os.environ.get("NEOEDGE_MARKET_CONFIG", "").strip()
    if market_config:
        if identity is None:
            print("serve: market worker requires the device identity/signing extra", file=sys.stderr)
            return 2
        from .adapters import marketplace_adapter
        from .adapters.marketplace import contract_no_for
        from .market_worker import (GrossoSettlementSink, MarketWorker, grammar_member_id,
                                    load_config)

        config = load_config(market_config)
        if grammar_member_id(config.member_id) != edge.member_id:
            print(f"serve: market config is for {config.member_id}, this book belongs to "
                  f"{edge.member_id}", file=sys.stderr)
            return 2
        adapter = marketplace_adapter()

        def record_handshake(event):
            actor, counterparty = event.get("actor"), event.get("counterparty")
            if config.member_id == actor:
                other, role = counterparty, "SELLER"
            elif config.member_id == counterparty:
                other, role = actor, "BUYER"
            else:
                raise ValueError("HANDSHAKE_NOT_FOR_THIS_EDGE")
            relation = edge.open_relation(contract_no_for(event["ref"]),
                                          grammar_member_id(other), role=role)
            edge.record(event, relation, adapter=adapter)

        grosso_url = os.environ.get("NEOEDGE_GROSSO_URL", "").strip()
        grosso_token = os.environ.get("NEOEDGE_GROSSO_TOKEN_FILE", "").strip()
        if bool(grosso_url) != bool(grosso_token):
            print("serve: NEOEDGE_GROSSO_URL and NEOEDGE_GROSSO_TOKEN_FILE must be set together",
                  file=sys.stderr)
            return 2
        settlement_sink = (GrossoSettlementSink(grosso_url, grosso_token)
                           if grosso_url else None)
        worker = MarketWorker(config, identity, on_handshake=record_handshake,
                              on_settlement=settlement_sink, state_dir=args.data)
        market_thread = threading.Thread(target=worker.run, args=(market_stop,),
                                         name="neoedgex-market-worker", daemon=True)
        market_thread.start()

    ui_host = os.environ.get("NEOEDGE_UI_HOST", "127.0.0.1")
    mcp_host = os.environ.get("NEOEDGE_MCP_HOST", "127.0.0.1")

    mcp = EdgeTCPServer(edge, host=mcp_host, port=args.mcp_port,
                        counterparty_hint=counterparty).start()
    server = EdgeMCPServer(edge)
    httpd = ThreadingHTTPServer(
        (ui_host, args.ui_port),
        make_handler(server, counterparty,
                     identity_public=identity.public_hex if identity else None),
    )

    print(f"NEOedgeX core · {edge.member_id}", file=sys.stderr)
    print(f"  UI    http://{ui_host}:{args.ui_port}   (the human's port)", file=sys.stderr)
    print(f"  MCP   {mcp.host}:{mcp.port}   (the agents' wire — never publish)", file=sys.stderr)
    print(f"  key   {identity.public_hex[:16] + '… (public half)' if identity else '(none)'}",
          file=sys.stderr)

    def stop(signum, frame):  # compose sends SIGTERM; answer it cleanly
        raise KeyboardInterrupt

    signal.signal(signal.SIGTERM, stop)
    try:
        httpd.serve_forever()
    except KeyboardInterrupt:
        pass
    finally:
        market_stop.set()
        if market_thread is not None:
            market_thread.join(timeout=5)
        mcp.shutdown()
        httpd.server_close()
    return 0


if __name__ == "__main__":
    sys.exit(main())
