"""Authenticated REA adapter executed on Hopper's dedicated Python thread.

The bootstrap injects ``REA_SOCKET`` and a random ``REA_TOKEN`` before executing
this file with Hopper's supported ``--python`` launcher option. Keep all Hopper
API access on this thread: moving dispatch to a worker can deadlock Hopper.
"""

import json
import hmac
import os
import re
import sre_parse
import socket

MAX_LINE_BYTES = 10 * 1024 * 1024
BAD_ADDRESSES = (-1, 0xFFFFFFFFFFFFFFFF, None)
_selected_document = None
_search_inventory_cache = {}
MAX_SEARCH_PATTERN_LENGTH = 256
MAX_SEARCH_VALUE_LENGTH = 4096


def _hex(value):
    return "0x%x" % value


def _json_safe(value):
    """Project Hopper-specific Python values into the JSON protocol boundary."""
    if value is None or isinstance(value, (bool, int, float, str)):
        return value
    if isinstance(value, (list, tuple)):
        return [_json_safe(item) for item in value]
    if isinstance(value, dict):
        return {str(key): _json_safe(item) for key, item in value.items()}
    return str(value)


def _document(name=None):
    """Resolve an explicit or session-selected document without changing Hopper UI."""
    global _selected_document
    documents = Document.getAllDocuments()
    if name is not None:
        for candidate in documents:
            if candidate.getDocumentName() == name:
                return candidate
        raise ValueError("Unknown Hopper document")
    if _selected_document is not None:
        for candidate in documents:
            if candidate.getDocumentName() == _selected_document:
                return candidate
    current = Document.getCurrentDocument()
    if current is None:
        raise ValueError("No Hopper document is loaded")
    return current


def _address(document, value=None):
    """Resolve hexadecimal addresses first, then fall back to Hopper symbol names."""
    if value is None:
        return document.getCurrentAddress()
    if not isinstance(value, str):
        raise ValueError("Address must be a string")
    try:
        return int(value, 16)
    except ValueError:
        result = document.getAddressForName(value)
        if result in BAD_ADDRESSES:
            raise ValueError("Unknown Hopper address or name")
        return result


def _segment(document, address):
    result = document.getSegmentAtAddress(address)
    if result is None:
        raise ValueError("Address is outside every segment")
    return result


def _procedure(document, value=None):
    address = document.getCurrentAddress() if value is None else _address(document, value)
    result = _segment(document, address).getProcedureAtAddress(address)
    if result is None:
        raise ValueError("No procedure exists at the requested address")
    return result


def _procedure_name(procedure):
    entry = procedure.getEntryPoint()
    return procedure.getSegment().getNameAtAddress(entry) or _hex(entry)


def _procedure_identity(procedure):
    return {"address": _hex(procedure.getEntryPoint()), "name": _procedure_name(procedure)}


def _procedure_locals(procedure):
    """Project opaque Hopper local-variable objects into an exact public shape."""
    return [
        {
            "description": str(local),
            "provenance": "hopper-public-python-api",
        }
        for local in procedure.getLocalVariableList()
    ]


def _containing_procedure(document, address):
    segment = document.getSegmentAtAddress(address)
    if segment is None:
        return None, "outside_segments"
    procedure = segment.getProcedureAtAddress(address)
    return (procedure, None) if procedure is not None else (None, "not_in_procedure")


def _instruction_addresses(procedure, limit):
    result = []
    seen = set()
    segment = procedure.getSegment()
    truncated = False
    for block in procedure.basicBlockIterator():
        address = block.getStartingAddress()
        end = block.getEndingAddress()
        while address < end and address not in seen:
            if len(result) >= limit:
                truncated = True
                return result, truncated
            seen.add(address)
            instruction = segment.getInstructionAtAddress(address)
            if instruction is None:
                break
            result.append(address)
            length = instruction.getInstructionLength()
            if length <= 0:
                break
            address += length
    return result, truncated


def _procedure_references(document, params):
    procedure = _procedure(document, params.get("procedure"))
    direction = params.get("direction", "outgoing")
    offset = params.get("offset", 0)
    limit = params.get("limit", 100)
    max_instructions = params.get("max_instructions", 500)
    if direction not in ("incoming", "outgoing"):
        raise ValueError("direction must be incoming or outgoing")
    if not isinstance(offset, int) or isinstance(offset, bool) or offset < 0:
        raise ValueError("offset must be a non-negative integer")
    if not isinstance(limit, int) or isinstance(limit, bool) or limit < 1 or limit > 500:
        raise ValueError("limit must be an integer between 1 and 500")
    if not isinstance(max_instructions, int) or isinstance(max_instructions, bool) or max_instructions < 1 or max_instructions > 5000:
        raise ValueError("max_instructions must be an integer between 1 and 5000")
    addresses, scan_truncated = _instruction_addresses(procedure, max_instructions)
    edges = set()
    for address in addresses:
        segment = _segment(document, address)
        references = segment.getReferencesFromAddress(address) if direction == "outgoing" else segment.getReferencesOfAddress(address)
        for reference in references:
            edges.add((address, reference) if direction == "outgoing" else (reference, address))
    ordered = sorted(edges)
    items = []
    selected = ordered[offset:offset + limit]
    for source, target in selected:
        source_procedure, _ = _containing_procedure(document, source)
        target_procedure, _ = _containing_procedure(document, target)
        items.append({
            "source_address": _hex(source),
            "target_address": _hex(target),
            "source_procedure": _procedure_identity(source_procedure) if source_procedure is not None else None,
            "target_procedure": _procedure_identity(target_procedure) if target_procedure is not None else None,
            "kind": _unavailable("Hopper's public Python API does not classify reference kinds"),
        })
    next_offset = offset + len(selected)
    has_more = next_offset < len(ordered)
    truncated = scan_truncated or has_more
    return {
        "procedure": _procedure_identity(procedure), "direction": direction,
        "references": {"items": items, "total": None if scan_truncated else len(ordered), "returned": len(items), "truncated": truncated, "next_offset": next_offset if has_more and not scan_truncated else None},
        "instructions_scanned": len(addresses), "instruction_scan_truncated": scan_truncated,
    }


def _procedure_map(document):
    result = {}
    for segment in document.getSegmentsList():
        for index in range(segment.getProcedureCount()):
            procedure = segment.getProcedureAtIndex(index)
            result[_hex(procedure.getEntryPoint())] = _procedure_name(procedure)
    return result


def _strings(document):
    result = {}
    for segment in document.getSegmentsList():
        for value, address in segment.getStringsList():
            result[_hex(address)] = value
    return result


def _invalidate_search_inventory(document):
    """Discard derived names after analysis metadata changes."""
    document_id = id(document)
    for key in list(_search_inventory_cache):
        if key[0] == document_id:
            del _search_inventory_cache[key]


def _search_inventory(document, kind):
    """Cache an immutable, address-sorted inventory for an unchanged document."""
    key = (id(document), kind)
    inventory = _search_inventory_cache.get(key)
    if inventory is None:
        values = _procedure_map(document) if kind == "procedure" else _strings(document)
        inventory = tuple(sorted(values.items(), key=lambda item: int(item[0], 16)))
        _search_inventory_cache[key] = inventory
    return inventory


def _validate_regex_node(node, inside_repeat=False):
    """Reject regex structures with disproportionate or non-local evaluation cost."""
    forbidden = {
        sre_parse.ASSERT,
        sre_parse.ASSERT_NOT,
        sre_parse.GROUPREF,
        sre_parse.GROUPREF_EXISTS,
    }
    repeat_tokens = {sre_parse.MAX_REPEAT, sre_parse.MIN_REPEAT}
    possessive = getattr(sre_parse, "POSSESSIVE_REPEAT", None)
    if possessive is not None:
        repeat_tokens.add(possessive)
    for operation, argument in node:
        if operation in forbidden:
            raise ValueError("Regex lookarounds and backreferences are not supported")
        if operation in repeat_tokens:
            if inside_repeat:
                raise ValueError("Nested regex repetitions are not supported")
            minimum, maximum, child = argument
            if maximum == sre_parse.MAXREPEAT or maximum > 1000:
                raise ValueError("Unbounded or excessive regex repetitions are not supported")
            _validate_regex_node(child, True)
        elif operation == sre_parse.SUBPATTERN:
            _validate_regex_node(argument[-1], inside_repeat)
        elif operation == sre_parse.BRANCH:
            for branch in argument[1]:
                _validate_regex_node(branch, inside_repeat)


def _search_page(document, kind, params):
    pattern = params.get("pattern")
    if not isinstance(pattern, str) or not pattern or len(pattern) > MAX_SEARCH_PATTERN_LENGTH:
        raise ValueError("pattern must contain between 1 and 256 characters")
    mode = params.get("mode", "literal")
    if mode not in ("literal", "regex"):
        raise ValueError("mode must be literal or regex")
    case_sensitive = params.get("case_sensitive", False)
    if not isinstance(case_sensitive, bool):
        raise ValueError("case_sensitive must be a boolean")
    offset = params.get("offset", 0)
    limit = params.get("limit", 100)
    if not isinstance(offset, int) or isinstance(offset, bool) or offset < 0:
        raise ValueError("offset must be a non-negative integer")
    if not isinstance(limit, int) or isinstance(limit, bool) or limit < 1 or limit > 100:
        raise ValueError("limit must be an integer between 1 and 100")

    if mode == "literal":
        needle = pattern if case_sensitive else pattern.casefold()
        matches = lambda value: needle in (value if case_sensitive else value.casefold())
    else:
        try:
            parsed = sre_parse.parse(pattern)
            _validate_regex_node(parsed)
            expression = re.compile(pattern, 0 if case_sensitive else re.IGNORECASE)
        except re.error as error:
            raise ValueError("Invalid regex pattern") from error
        matches = lambda value: expression.search(value) is not None

    selected = []
    total = 0
    page_end = offset + limit
    for item in _search_inventory(document, kind):
        if not matches(item[1]):
            continue
        if offset <= total < page_end:
            selected.append(item)
        total += 1
    next_offset = offset + len(selected)
    has_more = next_offset < total
    return {
        "items": [
            {
                "address": address,
                "value": value[:MAX_SEARCH_VALUE_LENGTH],
                "value_truncated": len(value) > MAX_SEARCH_VALUE_LENGTH,
            }
            for address, value in selected
        ],
        "offset": offset,
        "limit": limit,
        "total": total,
        "next_offset": next_offset if has_more else None,
        "has_more": has_more,
    }


def _page(values, offset, limit):
    """Return one deterministically address-sorted page without crossing an unbounded map."""
    if not isinstance(offset, int) or isinstance(offset, bool) or offset < 0:
        raise ValueError("offset must be a non-negative integer")
    if not isinstance(limit, int) or isinstance(limit, bool) or limit < 1 or limit > 500:
        raise ValueError("limit must be an integer between 1 and 500")
    ordered = sorted(values.items(), key=lambda item: int(item[0], 16))
    selected = ordered[offset:offset + limit]
    total = len(ordered)
    next_offset = offset + len(selected)
    has_more = next_offset < total
    return {
        "items": [{"address": address, "value": value} for address, value in selected],
        "offset": offset,
        "limit": limit,
        "total": total,
        "next_offset": next_offset if has_more else None,
        "has_more": has_more,
    }


def _unavailable(reason):
    """Describe evidence the public Hopper API cannot truthfully provide."""
    return {"available": False, "reason": reason}


def _offset(params, name):
    value = params.get(name, 0)
    if not isinstance(value, int) or isinstance(value, bool) or value < 0:
        raise ValueError("%s must be a non-negative integer" % name)
    return value


def _collection_offset(params, name):
    values = params.get("collection_offset", {})
    if not isinstance(values, dict):
        raise ValueError("collection_offset must be an object")
    value = values.get(name, 0)
    if not isinstance(value, int) or isinstance(value, bool) or value < 0:
        raise ValueError("collection_offset.%s must be a non-negative integer" % name)
    return value


def _bounded(items, offset, limit, total=None, scan_truncated=False):
    selected = items[offset:offset + limit]
    known_total = len(items) if total is None and not scan_truncated else total
    has_more = offset + len(selected) < len(items)
    return {
        "items": selected,
        "total": known_total,
        "returned": len(selected),
        "truncated": scan_truncated or has_more,
        "next_offset": offset + len(selected) if has_more else None,
    }


def _name_map(document):
    result = {}
    for segment in document.getSegmentsList():
        for address in segment.getNamedAddresses():
            name = segment.getNameAtAddress(address)
            if name is not None:
                result[address] = name
    return result


def _assembly(procedure, limit=None):
    """Render bounded assembly while guarding against malformed instruction cycles."""
    lines = []
    segment = procedure.getSegment()
    seen = set()
    for block in procedure.basicBlockIterator():
        address = block.getStartingAddress()
        end = block.getEndingAddress()
        while address < end and address not in seen:
            seen.add(address)
            instruction = segment.getInstructionAtAddress(address)
            if instruction is None:
                break
            arguments = [instruction.getFormattedArgument(index) for index in range(instruction.getArgumentCount())]
            suffix = ", ".join(value for value in arguments if value is not None)
            lines.append("%s: %s%s" % (_hex(address), instruction.getInstructionString(), (" " + suffix) if suffix else ""))
            if limit is not None and len(lines) >= limit:
                return "\n".join(lines)
            length = instruction.getInstructionLength()
            if length <= 0:
                break
            address += length
    return "\n".join(lines)


def _analyze_function(document, params):
    """Collect a bounded, single-pass function dossier for agent callers."""
    procedure = _procedure(document, params.get("procedure"))
    limit = params.get("limit", 100)
    max_chars = params.get("max_pseudocode_chars", 20000)
    max_instructions = params.get("max_instructions", 500)
    pseudocode_offset = _offset(params, "pseudocode_offset")
    assembly_offset = _offset(params, "assembly_offset")
    if not isinstance(limit, int) or isinstance(limit, bool) or limit < 1 or limit > 500:
        raise ValueError("limit must be an integer between 1 and 500")
    if not isinstance(max_chars, int) or isinstance(max_chars, bool) or max_chars < 1 or max_chars > 100000:
        raise ValueError("max_pseudocode_chars must be between 1 and 100000")
    if not isinstance(max_instructions, int) or isinstance(max_instructions, bool) or max_instructions < 1 or max_instructions > 5000:
        raise ValueError("max_instructions must be between 1 and 5000")
    addresses, instruction_scan_truncated = _instruction_addresses(procedure, max_instructions)
    blocks = []
    all_blocks = list(procedure.basicBlockIterator())
    for block in all_blocks:
        successors = []
        for index in range(block.getSuccessorCount()):
            successor = block.getSuccessorAddressAtIndex(index)
            if successor not in BAD_ADDRESSES:
                successors.append(_hex(successor))
        blocks.append({
            "start": _hex(block.getStartingAddress()),
            "end": _hex(block.getEndingAddress()),
            "successors": sorted(set(successors), key=lambda value: int(value, 16)),
        })
    pseudo = procedure.decompile() or ""
    assembly_lines = _assembly(procedure).splitlines() if params.get("include_assembly", False) else []
    callers = sorted((_procedure_identity(item) for item in procedure.getAllCallerProcedures()), key=lambda item: int(item["address"], 16))
    callees = sorted((_procedure_identity(item) for item in procedure.getAllCalleeProcedures()), key=lambda item: int(item["address"], 16))
    comments = []
    edges = set()
    for address in addresses:
        segment = _segment(document, address)
        comment = segment.getCommentAtAddress(address)
        inline_comment = segment.getInlineCommentAtAddress(address)
        if comment:
            comments.append({"address": _hex(address), "kind": "comment", "text": comment})
        if inline_comment:
            comments.append({"address": _hex(address), "kind": "inline", "text": inline_comment})
        for target in segment.getReferencesFromAddress(address):
            edges.add((address, target))
        for source in segment.getReferencesOfAddress(address):
            edges.add((source, address))
    incoming = []
    outgoing = []
    procedure_addresses = set(addresses)
    for source, target in sorted(edges):
        source_procedure, _ = _containing_procedure(document, source)
        target_procedure, _ = _containing_procedure(document, target)
        item = {
            "source_address": _hex(source),
            "target_address": _hex(target),
            "source_procedure": _procedure_identity(source_procedure) if source_procedure is not None else None,
            "target_procedure": _procedure_identity(target_procedure) if target_procedure is not None else None,
            "kind": _unavailable("Hopper's public Python API does not classify reference kinds"),
        }
        if target in procedure_addresses and source not in procedure_addresses:
            incoming.append(item)
        if source in procedure_addresses:
            outgoing.append(item)
    string_map = {int(address, 16): value for address, value in _strings(document).items()}
    name_map = _name_map(document)
    referenced_strings = []
    referenced_names = []
    for edge in outgoing:
        target = int(edge["target_address"], 16)
        if target in string_map:
            referenced_strings.append({"address": edge["target_address"], "value": string_map[target], "source_address": edge["source_address"]})
        if target in name_map:
            referenced_names.append({"address": edge["target_address"], "value": name_map[target], "source_address": edge["source_address"]})
    comments.sort(key=lambda item: (int(item["address"], 16), item["kind"]))
    referenced_strings.sort(key=lambda item: (int(item["address"], 16), int(item["source_address"], 16)))
    referenced_names.sort(key=lambda item: (int(item["address"], 16), int(item["source_address"], 16)))
    pseudo_text = pseudo[pseudocode_offset:pseudocode_offset + max_chars]
    pseudo_next = pseudocode_offset + len(pseudo_text)
    def collection(name, items, scan_limited=False):
        return _bounded(items, _collection_offset(params, name), limit, None, scan_limited)
    return {
        "procedure": {"address": _hex(procedure.getEntryPoint()), "name": _procedure_name(procedure), "signature": procedure.signatureString(), "locals": _procedure_locals(procedure)},
        "pseudocode": {"text": pseudo_text, "total_chars": len(pseudo), "returned_chars": len(pseudo_text), "truncated": pseudo_next < len(pseudo), "next_offset": pseudo_next if pseudo_next < len(pseudo) else None},
        "assembly": _bounded(assembly_lines, assembly_offset, max_instructions),
        "comments": collection("comments", comments, instruction_scan_truncated),
        "callers": collection("callers", callers), "callees": collection("callees", callees),
        "incoming_references": collection("incoming_references", incoming, instruction_scan_truncated),
        "outgoing_references": collection("outgoing_references", outgoing, instruction_scan_truncated),
        "referenced_strings": collection("referenced_strings", referenced_strings, instruction_scan_truncated),
        "referenced_names": collection("referenced_names", referenced_names, instruction_scan_truncated),
        "basic_blocks": collection("basic_blocks", blocks),
        "instruction_scan": {"scanned": len(addresses), "truncated": instruction_scan_truncated},
    }


def _dispatch(method, params):
    """Dispatch only the closed operation set implemented by REA's public tools."""
    global _selected_document
    if method == "health":
        return {"name": "REA Hopper bridge", "version": "1.0.0", "run_id": REA_RUN_ID}
    if method == "shutdown":
        return {"shutdown": True}
    if method == "list_documents":
        return [document.getDocumentName() for document in Document.getAllDocuments()]
    if method == "current_document":
        return _document().getDocumentName()
    if method == "set_current_document":
        document = _document(params.get("document"))
        _selected_document = document.getDocumentName()
        return _selected_document

    document = _document(params.get("document"))

    if method == "analyze_function":
        return _analyze_function(document, params)
    if method == "resolve_containing_procedure":
        address = _address(document, params.get("address"))
        procedure, reason = _containing_procedure(document, address)
        if procedure is None:
            return {"query_address": _hex(address), "found": False, "procedure": None, "reason": reason}
        return {"query_address": _hex(address), "found": True, "procedure": _procedure_identity(procedure)}
    if method == "procedure_references":
        return _procedure_references(document, params)

    if method == "current_address":
        return _hex(document.getCurrentAddress())
    if method == "current_procedure":
        return _procedure_name(_procedure(document))
    if method == "goto_address":
        address = _address(document, params.get("address"))
        document.moveCursorAtAddress(address)
        return _hex(address)
    if method in ("address_name", "comment", "inline_comment", "xrefs"):
        target = _address(document, params.get("address"))
        segment = _segment(document, target)
        if method == "address_name":
            return segment.getNameAtAddress(target)
        if method == "comment":
            return segment.getCommentAtAddress(target)
        if method == "inline_comment":
            return segment.getInlineCommentAtAddress(target)
        return [_hex(value) for value in segment.getReferencesOfAddress(target)]
    if method in ("next_address", "prev_address"):
        target = _address(document, params.get("address"))
        if method == "next_address":
            result = target + max(1, document.getObjectLength(target))
        else:
            result = document.getInstructionStart(max(0, target - 1))
        if result in BAD_ADDRESSES:
            raise ValueError("No adjacent address")
        return _hex(result)
    if method == "list_segments":
        result = []
        permission_limitation = _unavailable(
            "Hopper's public Python API does not expose segment or section permissions"
        )
        for segment in document.getSegmentsList():
            start = segment.getStartingAddress()
            sections = [{
                "name": section.getName(),
                "start": _hex(section.getStartingAddress()),
                "end": _hex(section.getStartingAddress() + section.getLength()),
                "readable": None,
                "writable": None,
                "executable": None,
                "permissions": permission_limitation,
                "provenance": "hopper-public-python-api",
            } for section in segment.getSectionsList()]
            result.append({
                "name": segment.getName(),
                "start": _hex(start),
                "end": _hex(start + segment.getLength()),
                "readable": None,
                "writable": None,
                "executable": None,
                "permissions": permission_limitation,
                "provenance": "hopper-public-python-api",
                "sections": sections,
            })
        return result
    if method == "list_procedures":
        return _page(_procedure_map(document), params.get("offset", 0), params.get("limit", 100))
    if method == "list_strings":
        values = _strings(document)
        requested = params.get("address")
        if requested is not None:
            key = _hex(_address(document, requested))
            values = {key: values[key]} if key in values else {}
        return _page(values, params.get("offset", 0), params.get("limit", 100))
    if method == "list_names":
        result = {}
        for segment in document.getSegmentsList():
            for item in segment.getNamedAddresses():
                result[_hex(item)] = segment.getNameAtAddress(item)
        requested = params.get("address")
        if requested is not None:
            key = _hex(_address(document, requested))
            result = {key: result[key]} if key in result else {}
        return _page(result, params.get("offset", 0), params.get("limit", 100))
    if method in ("search_procedures", "search_strings"):
        kind = "procedure" if method == "search_procedures" else "string"
        return _search_page(document, kind, params)
    if method.startswith("procedure_"):
        procedure = _procedure(document, params.get("procedure"))
        if method == "procedure_address":
            return _hex(procedure.getEntryPoint())
        if method == "procedure_assembly":
            return _assembly(procedure)
        if method == "procedure_pseudo_code":
            return procedure.decompile()
        if method == "procedure_callers":
            return [_procedure_name(item) for item in procedure.getAllCallerProcedures()]
        if method == "procedure_callees":
            return [_procedure_name(item) for item in procedure.getAllCalleeProcedures()]
        if method == "procedure_info":
            blocks = list(procedure.basicBlockIterator())
            length = sum(max(0, block.getEndingAddress() - block.getStartingAddress()) for block in blocks)
            return {"name": _procedure_name(procedure), "entrypoint": _hex(procedure.getEntryPoint()), "basicblock_count": procedure.getBasicBlockCount(), "length": length, "signature": procedure.signatureString(), "locals": _procedure_locals(procedure)}
    if method == "set_address_name":
        address = _address(document, params.get("address"))
        result = document.setNameAtAddress(address, params["name"])
        _invalidate_search_inventory(document)
        return result
    if method == "set_addresses_names":
        result = {key: document.setNameAtAddress(_address(document, key), value) for key, value in params["names"].items()}
        _invalidate_search_inventory(document)
        return result
    if method in ("set_comment", "set_inline_comment"):
        address = _address(document, params.get("address"))
        segment = _segment(document, address)
        setter = segment.setCommentAtAddress if method == "set_comment" else segment.setInlineCommentAtAddress
        getter = segment.getCommentAtAddress if method == "set_comment" else segment.getInlineCommentAtAddress
        setter(address, params["comment"])
        return getter(address) == params["comment"]
    if method == "list_bookmarks":
        return [{"address": _hex(item), "name": document.getBookmarkName(item)} for item in document.getBookmarks()]
    if method == "set_bookmark":
        address = _address(document, params.get("address"))
        document.setBookmarkAtAddress(address, params.get("name"))
        return document.hasBookmarkAtAddress(address)
    if method == "unset_bookmark":
        address = _address(document, params.get("address"))
        document.removeBookmarkAtAddress(address)
        return not document.hasBookmarkAtAddress(address)
    raise ValueError("Unknown bridge method")


def _serve_connection(connection):
    """Serve one size-bounded, capability-authenticated NDJSON connection."""
    file = connection.makefile("rwb")
    while True:
        line = file.readline(MAX_LINE_BYTES + 1)
        if not line or len(line) > MAX_LINE_BYTES:
            break
        request_id = None
        should_stop = False
        try:
            request = json.loads(line.decode("utf-8"))
            if set(request) != {"id", "token", "method", "params"}:
                raise ValueError("Invalid bridge request shape")
            request_id = request["id"]
            if not isinstance(request["token"], str) or not hmac.compare_digest(request["token"], REA_TOKEN):
                raise PermissionError("Invalid bridge capability")
            result = _dispatch(request["method"], request["params"])
            should_stop = request["method"] == "shutdown"
            response = {"id": request_id, "result": _json_safe(result)}
        except Exception as error:
            response = {"id": request_id if isinstance(request_id, int) else 0, "error": {"code": -32000, "message": str(error)[:512], "type": _diagnostic_type(error)}}
        file.write((json.dumps(response, separators=(",", ":")) + "\n").encode("utf-8"))
        file.flush()
        if should_stop:
            break
    file.close()
    connection.close()


def _diagnostic_type(error):
    if isinstance(error, PermissionError):
        return "authorization"
    if isinstance(error, (ValueError, TypeError, KeyError)):
        return "invalid_request"
    return "bridge_exception"


def _run():
    """Own a permission-restricted, single-client Unix socket for this bridge."""
    if os.path.exists(REA_SOCKET):
        os.unlink(REA_SOCKET)
    server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
    server.bind(REA_SOCKET)
    os.chmod(REA_SOCKET, 0o600)
    server.listen(1)
    try:
        connection, _ = server.accept()
        _serve_connection(connection)
    finally:
        server.close()
        if os.path.exists(REA_SOCKET):
            os.unlink(REA_SOCKET)


# Hopper's public objects are bound to its dedicated Python execution thread.
# Keep dispatch on that thread; moving calls to an arbitrary worker can deadlock.
_run()
