"""Process-wide side effects requested by linter fixes.

The abstra-upgrade fix needs to restart the EDITOR process after pip swaps
site-packages. With the linter sidecar, fixes run in the child process, where
os._exit/os.execv would restart the wrong process. Fixes therefore call
request_process_action() instead of exiting directly:

- default handler (editor process / in-process kill-switch path): executes the
  action immediately — byte-identical to the historical behavior;
- the sidecar server installs a collector handler, ships the action back in
  the apply_fix RPC response, and the editor-side client executes it there.
"""

import os
import sys
import threading
from typing import Callable, Optional

from abstra_internals.environment import EDITOR_MODE, RABBITMQ_CONNECTION_URI
from abstra_internals.logger import AbstraLogger
from abstra_internals.repositories.producer import WebEditorControlProducerRepository

RESTART_EDITOR = "restart_editor"
# A bare signal (no payload): the editor records that a restart is pending and
# re-broadcasts editor-status. Unlike RESTART_EDITOR it does NOT restart — the
# user applies it later via "Restart editor". It carries no data because the
# restart never needs the package names, and the UI shows a generic message.
MARK_NEEDS_RESTART = "mark_needs_restart"


# User-facing warning appended to issue labels whose fix pip-installs and thus
# triggers restart_editor_and_workers. Lives here so every rule that installs
# packages warns with the same copy.
RESTART_NOTICE = "Installing will restart the editor and may take up to 2 minutes."

_handler_lock = threading.Lock()
_handler: Optional[Callable[[str], None]] = None


def set_process_action_handler(handler: Optional[Callable[[str], None]]) -> None:
    """Install a handler that intercepts process actions (None restores the
    default execute-immediately behavior)."""
    global _handler
    with _handler_lock:
        _handler = handler


def request_process_action(action: str, reason: Optional[str] = None) -> None:
    with _handler_lock:
        handler = _handler
    if handler is not None:
        # The collector RPC only ships the action string, so `reason` is lost
        # on the sidecar path; the direct path keeps it for lifecycle logging.
        handler(action)
        return
    execute_process_action(action, reason=reason)


def _mark_needs_restart() -> None:
    """Editor-side handler for MARK_NEEDS_RESTART: record that a restart is
    pending (a dependency was installed) and re-broadcast editor-status so the
    "Restart editor" button appears. Runs in the editor process (directly, or via
    the sidecar client executing the action shipped back from the child). Imports
    are local to avoid an import cycle (editor_restart imports this module)."""
    from abstra_internals.controllers.editor_restart import EditorRestartController
    from abstra_internals.controllers.editor_status_events import (
        EditorStatusEventController,
    )

    EditorRestartController.mark_dependencies_installed()
    AbstraLogger.warning(
        "[ProcessAction] Marked a restart as pending (dependency install)"
    )
    EditorStatusEventController.broadcast()


def execute_process_action(
    action: str, is_web: Optional[bool] = None, reason: Optional[str] = None
) -> None:
    if action == MARK_NEEDS_RESTART:
        _mark_needs_restart()
        return

    if action != RESTART_EDITOR:
        AbstraLogger.warning(f"[ProcessAction] Unknown process action: {action}")
        return

    if is_web is None:
        from abstra_internals.environment import EDITOR_MODE

        is_web = EDITOR_MODE == "web"

    if is_web:
        AbstraLogger.lifecycle(
            "[Editor] Exiting for an intentional restart",
            {
                "stage": "editor.restart_requested",
                "action": action,
                "reason": reason,
            },
        )
        # sys.exit() doesn't work because Flask runs in threaded mode;
        # os._exit() terminates directly and the kubelet restarts the pod.
        AbstraLogger.warning("[ProcessAction] Exiting editor for the pod to restart")
        os._exit(0)
    else:
        AbstraLogger.warning("[ProcessAction] Restarting editor in place")
        os.execv(sys.executable, [sys.executable] + sys.argv)


def restart_editor_and_workers(log_prefix: str) -> None:
    """Restart the worker tier and then the editor after a pip install.

    A freshly ``pip install``ed package is on disk but invisible to any process
    that fixed its ``sys.path`` / imported its modules before the install — and
    compiled deps (numpy/pandas) can't be reliably reloaded in-process. So the
    only dependable way to make new packages visible to BOTH tiers is a restart:

    - workers (forkserver + forked executors) reload via the RabbitMQ control
      broadcast, so user code runs against the new packages;
    - the editor restarts via the process-action hook, which respawns the
      linter sidecar so its checks re-discover the packages.

    Same sequence the abstra-upgrade fix uses; workers first, then the editor.
    """
    if EDITOR_MODE == "web" and RABBITMQ_CONNECTION_URI:
        try:
            WebEditorControlProducerRepository(
                RABBITMQ_CONNECTION_URI
            ).restart_workers()
            AbstraLogger.warning(f"{log_prefix} Sent restart signal to workers")
        except Exception as e:
            AbstraLogger.error(
                f"{log_prefix} Failed to send restart signal to workers: {e}"
            )

    AbstraLogger.warning(f"{log_prefix} Requesting editor restart")
    request_process_action(RESTART_EDITOR, reason=log_prefix.strip("[]"))


def restart_or_defer_after_install() -> None:
    """Make a just-installed dependency take effect. On the web editor the
    restart is DEFERRED: mark a restart as pending (the editor surfaces a
    "Restart editor" button) instead of dropping the user's session now — a
    worker respawned before the restart just sees the package on disk early,
    which is harmless. Elsewhere (desktop) restart immediately so the long-lived
    processes can import it.

    Unlike the abstra self-update, deferring a dependency install needs no boot
    shim (there is no slot to flip — the package is already on disk), so the gate
    is web vs. not, not web+shim."""
    if EDITOR_MODE == "web":
        request_process_action(MARK_NEEDS_RESTART)
    else:
        restart_editor_and_workers("[InstallPackage]")
