"""The local UX — the edge on the customer's own screen, v0.

    python3 -m neoedge.ui --demo                 # a testbed edge with one real dispute
    python3 -m neoedge.ui --book book.jsonl      # a live book, read-only
    (then open http://127.0.0.1:8377)

One law carries this file: **the UI reads through `EdgeMCPServer` and nothing else.** The
MCP surface already enforces what leaves an edge — figures withheld and NAMED as withheld,
documents never — so the page cannot show more than an MCP host could learn. One disclosure
surface, not two; a second path is how a leak gets built by accident.

Serving posture: `127.0.0.1` only, stdlib `http.server`, read-only handlers (`GET` renders,
one `POST /review` runs the L0 comparison on counterparty records the user pastes — records
the counterparty handed THEM; an edge never holds the other side's book). No JS frameworks,
no external assets, no cookies: the page is the company's own facts, styled enough to be
read with morning coffee.
"""

from __future__ import annotations

import argparse
import html
import json
import sys
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import parse_qs

from .mcp.server import EdgeMCPServer

DEFAULT_PORT = 8377

_STYLE = """
:root { --bg:#f6f4ef; --card:#fffdf8; --ink:#1c1b18; --dim:#6f6a5e; --line:#e4dfd2;
        --good:#2e6e4e; --bad:#a03a2a; --accent:#3d5a80; }
@media (prefers-color-scheme: dark) {
  :root { --bg:#171614; --card:#201e1b; --ink:#ece7dc; --dim:#9a938a; --line:#37332c;
          --good:#7fc8a0; --bad:#e08d7d; --accent:#8fb3d9; } }
* { box-sizing:border-box; }
body { margin:0; padding:2rem 1.25rem 4rem; background:var(--bg); color:var(--ink);
       font:15px/1.55 -apple-system, "Segoe UI", system-ui, sans-serif;
       max-width:64rem; margin-inline:auto; }
h1 { font-size:1.35rem; margin:0 0 .25rem; } h2 { font-size:1.02rem; margin:2rem 0 .6rem; }
.sub { color:var(--dim); margin-bottom:1.5rem; }
.cards { display:flex; gap:.75rem; flex-wrap:wrap; }
.card { background:var(--card); border:1px solid var(--line); border-radius:10px;
        padding: .8rem 1.1rem; min-width:9rem; }
.card b { display:block; font-size:1.35rem; } .card span { color:var(--dim); font-size:.82rem; }
table { border-collapse:collapse; width:100%; background:var(--card);
        border:1px solid var(--line); border-radius:10px; overflow:hidden; }
th, td { text-align:left; padding:.45rem .7rem; border-top:1px solid var(--line);
         font-size:.88rem; vertical-align:top; }
th { border-top:none; color:var(--dim); font-weight:600; font-size:.78rem;
     text-transform:uppercase; letter-spacing:.04em; }
code { font:12px/1.4 ui-monospace, Menlo, monospace; word-break:break-all; }
.ok { color:var(--good); font-weight:600; } .warn { color:var(--bad); font-weight:600; }
.pill { display:inline-block; border:1px solid var(--line); border-radius:99px;
        padding:.05rem .55rem; font-size:.75rem; color:var(--dim); }
textarea { width:100%; min-height:7rem; background:var(--card); color:var(--ink);
           border:1px solid var(--line); border-radius:10px; padding:.7rem;
           font:12px/1.5 ui-monospace, Menlo, monospace; }
button { margin-top:.5rem; background:var(--accent); color:#fff; border:none;
         border-radius:8px; padding:.5rem 1.1rem; font-size:.9rem; cursor:pointer; }
pre.report { background:var(--card); border:1px solid var(--line); border-radius:10px;
             padding:1rem; font:12px/1.6 ui-monospace, Menlo, monospace; overflow-x:auto; }
footer { margin-top:3rem; color:var(--dim); font-size:.78rem; }
"""


def _esc(value) -> str:
    return html.escape(str(value))


def render_page(server: EdgeMCPServer, *, demo_counterparty: list[dict] | None = None,
                review: dict | None = None, review_error: str | None = None,
                identity_public: str | None = None) -> str:
    """The whole page from the MCP surface — pure, so tests read it without a socket."""
    status = server.call("edge_status")["content"]
    relations = server.call("edge_relations")["content"]
    records = server.call("edge_records")["content"]

    verifies = ('<span class="ok">chain verifies</span>' if status["verifies"]
                else '<span class="warn">CHAIN BROKEN</span>')
    sealed = ", ".join(map(_esc, status["sealed_periods"])) or "—"

    rel_rows = "".join(
        f"<tr><td><code>{_esc(r['relation_ref'][:16])}…</code></td>"
        f"<td>{_esc(r.get('counterparty') or '—')}</td><td>{_esc(r.get('role') or '—')}</td>"
        f"<td>{_esc(r.get('contract_no') or '—')}</td></tr>"
        for r in relations) or '<tr><td colspan="4">no relations yet</td></tr>'

    rec_rows = "".join(
        f"<tr><td>{_esc(r.get('doc_kind'))}</td><td>{_esc(r.get('originator_doc_no'))}</td>"
        f"<td>{_esc(r.get('doc_date'))}</td>"
        f"<td>{_esc(r.get('line_count', ''))}</td>"
        f"<td>{''.join(f'<span class=pill>{_esc(w)}</span> ' for w in r.get('withheld', []))}</td></tr>"
        for r in records) or '<tr><td colspan="5">the book is empty</td></tr>'

    review_html = ""
    if review_error:
        review_html = f'<p class="warn">{_esc(review_error)}</p>'
    elif review is not None:
        verdict = ('<span class="ok">the whole relation AGREES</span>' if review["agreed"]
                   else f'<span class="warn">{len(review["disagreements"])} disagreement(s)</span>')
        review_html = f"<p>{verdict}</p><pre class=\"report\">{_esc(review['text'])}</pre>"

    demo_note = ""
    default_cp = ""
    if demo_counterparty is not None:
        demo_note = ('<p class="sub">demo mode: the counterparty\'s records are pre-filled '
                     "below — in real use the other party hands them over.</p>")
        default_cp = _esc(json.dumps(demo_counterparty, indent=1))

    relation_options = "".join(
        f'<option value="{_esc(r["relation_ref"])}">{_esc((r.get("contract_no") or r["relation_ref"][:16]))}'
        f' · {_esc(r.get("counterparty") or "?")}</option>' for r in relations)

    return f"""<!doctype html><html><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>NEOedgeX — {_esc(status['member_id'])}</title><style>{_STYLE}</style></head><body>
<h1>NEOedgeX</h1>
<p class="sub">{_esc(status['member_id'])} · read-only view through the edge's own MCP surface —
figures and documents are withheld here exactly as they are for any AI host.
{f'<span class="pill">device {_esc(identity_public[:16])}… (public half)</span>' if identity_public else ''}</p>
<div class="cards">
  <div class="card"><b>{status['book_height']}</b><span>records in the book</span></div>
  <div class="card"><b>{len(relations)}</b><span>relations</span></div>
  <div class="card"><b>{len(status['sealed_periods'])}</b><span>sealed periods · {sealed}</span></div>
  <div class="card"><b>{verifies}</b><span>re-derived, not trusted</span></div>
</div>
<h2>Relations</h2>
<table><tr><th>relation</th><th>counterparty</th><th>role</th><th>contract</th></tr>{rel_rows}</table>
<h2>Records <span class="pill">withheld fields are named, never silently dropped</span></h2>
<table><tr><th>kind</th><th>originator no</th><th>date</th><th>lines</th><th>withheld</th></tr>{rec_rows}</table>
<h2>Review — settle it yourselves (L0)</h2>
{demo_note}
<form method="post" action="/review">
  <p><select name="relation_ref">{relation_options}</select></p>
  <textarea name="counterparty_records" placeholder="paste the counterparty's records (JSON array)">{default_cp}</textarea>
  <br><button type="submit">compare the two books</button>
</form>
{review_html}
<footer>NEOedgeX · documents never leave · volume never leaves · only agreement does</footer>
</body></html>"""


def make_handler(server: EdgeMCPServer, demo_counterparty: list[dict] | None,
                 identity_public: str | None = None):
    class Handler(BaseHTTPRequestHandler):
        def _send(self, body: str, code: int = 200) -> None:
            data = body.encode("utf-8")
            self.send_response(code)
            self.send_header("Content-Type", "text/html; charset=utf-8")
            self.send_header("Content-Length", str(len(data)))
            self.end_headers()
            self.wfile.write(data)

        def do_GET(self):  # noqa: N802 — http.server's spelling
            if self.path not in ("/", "/index.html"):
                self._send("<h1>404</h1>", 404)
                return
            self._send(render_page(server, demo_counterparty=demo_counterparty,
                                   identity_public=identity_public))

        def do_POST(self):  # noqa: N802
            if self.path != "/review":
                self._send("<h1>404</h1>", 404)
                return
            length = int(self.headers.get("Content-Length", 0))
            form = parse_qs(self.rfile.read(length).decode("utf-8"))
            relation_ref = (form.get("relation_ref") or [""])[0]
            raw = (form.get("counterparty_records") or [""])[0]
            review = error = None
            try:
                counterparty = json.loads(raw) if raw.strip() else []
                out = server.call("edge_review", {"relation_ref": relation_ref,
                                                  "counterparty_records": counterparty})
                if out["isError"]:
                    error = str(out["content"])
                else:
                    review = out["content"]
            except json.JSONDecodeError as exc:
                error = f"that is not JSON: {exc}"
            self._send(render_page(server, demo_counterparty=demo_counterparty,
                                   review=review, review_error=error,
                                   identity_public=identity_public))

        def log_message(self, fmt, *args):  # stderr, and quiet about it
            print(f"ui: {fmt % args}", file=sys.stderr)

    return Handler


def main(argv: list[str] | None = None) -> int:
    ap = argparse.ArgumentParser(prog="python3 -m neoedge.ui")
    ap.add_argument("--demo", action="store_true", help="serve a testbed edge with one dispute")
    ap.add_argument("--book", help="serve a live book file, read-only")
    ap.add_argument("--port", type=int, default=DEFAULT_PORT)
    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

    server = EdgeMCPServer(edge)
    httpd = ThreadingHTTPServer(("127.0.0.1", args.port), make_handler(server, counterparty))
    print(f"NEOedgeX UI · http://127.0.0.1:{args.port} · serving {edge.member_id} "
          f"(local only, read-only)", file=sys.stderr)
    try:
        httpd.serve_forever()
    except KeyboardInterrupt:
        pass
    return 0


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