"""One stdlib JSON-line PM request per isolated process."""
from __future__ import annotations

import json
import os
from pathlib import Path
import queue
import sys
import threading


def _read_controls(messages, pause, fd):
    # Raw reads avoid a daemon thread holding sys.stdin's buffered lock at exit.
    pending = b""
    request_id = None
    try:
        while block := os.read(fd, 65536):
            pending += block
            while b"\n" in pending:
                line, pending = pending.split(b"\n", 1)
                message = json.loads(line)
                if request_id is None:
                    request_id = message["id"]
                if message["id"] != request_id:
                    raise ValueError("unexpected PM control request id")
                if message.get("cancel"):
                    pause.set()
                if message.get("type") != "cancel":
                    messages.put(message)
    except (OSError, ValueError, KeyError) as exc:
        messages.put(exc)
    finally:
        os.close(fd)
        pause.set()
        messages.put(None)


def main():
    import truststore

    truststore.inject_into_ssl()
    # Capture the protocol FD before redirecting even native/subprocess stdout.
    wire = os.fdopen(os.dup(sys.stdout.fileno()), "w", encoding="utf-8", buffering=1)
    os.dup2(sys.stderr.fileno(), sys.stdout.fileno())
    sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
    # A pending read on inherited control stdin can block child Python startup
    # on Windows. Keep the protocol private and give every ordinary child EOF.
    controls = os.dup(0)
    os.set_inheritable(controls, False)
    with open(os.devnull, "rb") as null:
        os.dup2(null.fileno(), 0)
    messages = queue.Queue()
    pause = threading.Event()
    threading.Thread(target=_read_controls, args=(messages, pause, controls), daemon=True).start()

    def receive():
        message = messages.get()
        if message is None:
            raise RuntimeError("PM client disconnected")
        if isinstance(message, Exception):
            raise message
        return message

    request = receive()
    from pm import paths, plugin_inputs, receipt
    from pm.package import InstallError
    from pm.registry import load_package_definitions
    from pm.runtime import lease_current_runtime
    from pm.worker_operations import OPERATIONS
    lease_current_runtime()
    context = request["context"]
    paths.repo_root = lambda: Path(context["repo"])
    paths.lockfile_path = lambda: Path(context["lockfile"])

    call = 0
    callback_lock = threading.Lock()

    def send(data):
        wire.write(json.dumps({"id": request["id"], **data}) + "\n")

    def callback(name, *args):
        with callback_lock:
            return exchange(name, args)

    def exchange(name, args):
        nonlocal call
        call += 1
        send({"type": "callback", "callback": name, "call": call, "args": args})
        reply = receive()
        if reply["id"] != request["id"] or reply["call"] != call:
            raise RuntimeError("unexpected PM callback response")
        if "error" in reply:
            raise RuntimeError(reply["error"])
        return reply["result"]

    with receipt.worker_context(request.get("update_id")):
        try:
            load_package_definitions(request.get("packages", []))
            operation = request["operation"]
            implementation = OPERATIONS[operation].resolve(operation)
            arguments = request["arguments"]
            if request["operation"] in ("sync_venv", "venv_is_current"):
                arguments["plugins"] = plugin_inputs.decode(arguments.get("plugins"))
            if request["operation"] == "ensure":
                arguments["pause_event"] = pause
            for name in ("progress", "download_progress"):
                if name in request["callbacks"]:
                    arguments[name] = lambda *args, name=name: callback(name, *args)
            result = implementation(**arguments)
            if request["operation"] == "ensure":
                result = None  # Runner is reconstructed from the caller's base env.
            if isinstance(result, Path):
                result = str(result)
            response = {"result": result}
        except BaseException as exc:
            error = {"type": type(exc).__name__, "message": str(exc)}
            if isinstance(exc, InstallError):
                error.update(package=exc.package, cause=exc.cause, remedy=exc.remedy)
            response = {"error": error}
        response["receipt"] = receipt.last_completed()
    send({"type": "result", **response})


if __name__ == "__main__":
    main()
