import os
import select
import sys
import time
import traceback
from collections import defaultdict, namedtuple
from concurrent.futures import Future
from enum import Enum
from queue import Queue
from loguru import logger
from .Activity import Activity
from .CentralDispatch import CentralDispatch, SerialDispatchQueue
from .EventTypes import (
    StopApplication, ExceptionOccured, KeyStroke, KeyRelease, KeyState,
    MouseDown, MouseUp, MouseClick, MouseScroll, MouseDrag, MouseMove, MouseButton,
)
from .Service import Service, ServiceState
from . import Keys
from .BlessedInput import blessed_key_to_int, blessed_keystroke_state
from .activities.LogViewerActivity import LogViewerActivity
from .activities.ShowExceptionActivity import ShowExceptionActivity


class _RawMouseMotion:
    """Internal-only: motion event with a button held (pre-synthesis)."""
    def __init__(self, button, x, y, modifiers=0):
        self.button = button
        self.x = x
        self.y = y
        self.modifiers = modifiers


class Segue(Enum):
    PUSH = 0
    REPLACE = 1


LabeledCallback = namedtuple("LabeledCallback", ["label", "callback"])


class Application:
    def __init__(self, screen=None, enable_kitty=False, enable_mouse=False):
        self.log_filename = "application.log"
        self.curses_screen = screen  # None means "create BlessedScreen on start()"
        self._enable_kitty = enable_kitty
        self._enable_mouse = enable_mouse
        self.event_subscribers = defaultdict(set)
        self.stack = []

        self.event_queue = Queue()

        self.shutdown_signal: Future = None
        self.main_thread: SerialDispatchQueue = None
        self.background_thread: SerialDispatchQueue = None

        self.last_exception = None

        self._services = {}  # name -> Service
        self._term = None
        self._term_contexts = []
        self._kitty_active = False
        self._mouse_active = False
        self._mouse_drag_state = None
        self._mouse_last_down = None
        self._last_kitty_repeat_time = 0.0

    # -- Service lifecycle -------------------------------------------------

    def register_service(self, name, service):
        """Register a service by name.  Must be called before start_service."""
        if name in self._services:
            raise ValueError(f"Service {name!r} is already registered")
        service._application = self
        self._services[name] = service

    def service(self, name):
        """Return the Service instance registered under *name*."""
        return self._services[name]

    def start_service(self, name, on_started=None):
        """Start a service asynchronously on its own thread.

        If *on_started* is provided it is called on the main thread once the
        service reaches RUNNING state.
        """
        svc = self._services[name]
        if svc._state not in (ServiceState.IDLE, ServiceState.STOPPED):
            raise RuntimeError(
                f"Cannot start service {name!r} in state {svc._state.value}"
            )

        # Transition to STARTING immediately so a second call sees it.
        svc._state = ServiceState.STARTING
        svc._running_event.clear()
        svc._stopped_event.clear()

        def _do_start():
            try:
                svc.on_start()
                svc._state = ServiceState.RUNNING
                logger.info(f"Service {name!r} started")
                if on_started and self.main_thread:
                    self.main_thread.submit_async(on_started)
                svc._running_event.set()
            except Exception as e:
                svc._state = ServiceState.STOPPED
                svc._stopped_event.set()
                logger.error(f"Service {name!r} failed to start: {e}")
                raise

        return CentralDispatch.future(_do_start)

    def stop_service(self, name, on_stopped=None):
        """Stop a running service asynchronously on its own thread."""
        svc = self._services[name]
        if svc._state != ServiceState.RUNNING:
            raise RuntimeError(
                f"Cannot stop service {name!r} in state {svc._state.value}"
            )

        # Transition to STOPPING immediately so a second call sees it.
        svc._state = ServiceState.STOPPING
        svc._running_event.clear()

        def _do_stop():
            try:
                svc.on_stop()
            finally:
                svc._state = ServiceState.STOPPED
                logger.info(f"Service {name!r} stopped")
                if on_stopped and self.main_thread:
                    self.main_thread.submit_async(on_stopped)
                svc._stopped_event.set()

        return CentralDispatch.future(_do_stop)

    def restart_service(self, name, on_started=None):
        """Stop then start a service.  Returns a future that resolves when
        the service is running again."""
        svc = self._services[name]
        if svc._state not in (ServiceState.RUNNING, ServiceState.IDLE, ServiceState.STOPPED):
            raise RuntimeError(
                f"Cannot restart service {name!r} in state {svc._state.value}"
            )

        # Transition immediately: STOPPING if running, STARTING otherwise.
        was_running = svc._state == ServiceState.RUNNING
        svc._state = ServiceState.STOPPING if was_running else ServiceState.STARTING
        svc._running_event.clear()
        svc._stopped_event.clear()

        def _do_restart():
            if was_running:
                try:
                    svc.on_stop()
                finally:
                    svc._state = ServiceState.STOPPED
                    svc._stopped_event.set()
                    logger.info(f"Service {name!r} stopped (restart)")

            svc._state = ServiceState.STARTING
            svc._stopped_event.clear()
            try:
                svc.on_start()
                svc._state = ServiceState.RUNNING
                logger.info(f"Service {name!r} restarted")
                if on_started and self.main_thread:
                    self.main_thread.submit_async(on_started)
                svc._running_event.set()
            except Exception as e:
                svc._state = ServiceState.STOPPED
                svc._stopped_event.set()
                logger.error(f"Service {name!r} failed to restart: {e}")
                raise

        return CentralDispatch.future(_do_restart)

    def require_service(self, name, timeout=None):
        """Auto-start if IDLE/STOPPED, block until RUNNING, return the service."""
        svc = self._services[name]
        if svc._state in (ServiceState.IDLE, ServiceState.STOPPED):
            self.start_service(name)
        if not svc.wait_until_running(timeout=timeout):
            raise TimeoutError(f"Service {name!r} did not reach RUNNING state in time")
        return svc

    def _stop_all_services(self):
        """Synchronously stop all running services (called during shutdown)."""
        for name, svc in self._services.items():
            # Wait for in-flight transitions before deciding to stop.
            if svc._state == ServiceState.STARTING:
                svc.wait_until_running(timeout=5)
            if svc._state == ServiceState.RUNNING:
                try:
                    svc._state = ServiceState.STOPPING
                    svc.on_stop()
                except Exception as e:
                    logger.error(f"Error stopping service {name!r} during shutdown: {e}")
                finally:
                    svc._state = ServiceState.STOPPED
                    svc._running_event.clear()
                    svc._stopped_event.set()

    def handle_shutdown(self, shutdown_event):
        self._stop_all_services()
        if shutdown_event.exception:
            try:
                raise shutdown_event.exception
            except Exception as e:
                logger.info("Shutdown because of error:")
                logger.info(f"{e.__class__.__name__}: {e}")
                logger.info(traceback.format_exc())
        else:
            logger.info("Exited Normally")
        try:
            if self.main_thread:
                self.main_thread.finish_work().result()
                self.main_thread.task_threadpool.shutdown(wait=False)
        except Exception:
            pass
        try:
            if self.background_thread:
                self.background_thread.finish_work().result()
                self.background_thread.task_threadpool.shutdown(wait=False)
        except Exception:
            pass

    def subscribe(self, event_type, activity, callback):
        self.event_subscribers[event_type].add(LabeledCallback(activity, callback))

    def unsubscribe(self, event_type, from_activity):
        subscribers = self.event_subscribers.get(event_type, set())
        for labeled_callback in subscribers.copy():
            if labeled_callback.label == from_activity:
                subscribers.remove(labeled_callback)

    def unsubscribe_all(self, from_activity):
        for event_type, subscribers in self.event_subscribers.items():
            for labeled_callback in subscribers.copy():
                if labeled_callback.label == from_activity:
                    self.event_subscribers[event_type].remove(labeled_callback)

    def setup_logger(self):
        logger.remove()
        logger.add(self.log_filename, format="{time:HH:mm:ss} {module} {message}")

    def _setup_blessed_terminal(self):
        """Create a blessed Terminal and BlessedScreen, enter fullscreen."""
        import blessed
        from .BlessedScreen import BlessedScreen

        term = blessed.Terminal()
        self._term = term

        ctx_fullscreen = term.fullscreen()
        ctx_fullscreen.__enter__()
        self._term_contexts.append(ctx_fullscreen)

        ctx_cbreak = term.cbreak()
        ctx_cbreak.__enter__()
        self._term_contexts.append(ctx_cbreak)

        # Cursor visibility is managed per-frame by BlessedScreen.refresh():
        # shown and positioned when a text input is focused, hidden otherwise.

        self.curses_screen = BlessedScreen(term)

        # Detect and activate Kitty keyboard protocol if requested
        if self._enable_kitty:
            if self._detect_kitty_support():
                self._kitty_active = True
                # Push mode: disambiguate (1) + report event types (2) = 3
                sys.stdout.write("\x1b[>3u")
                sys.stdout.flush()
                logger.info("Kitty keyboard protocol activated")
            else:
                logger.info("Kitty keyboard protocol not supported; "
                            "using synthetic releases")

        if self._enable_mouse:
            sys.stdout.write("\x1b[?1006h")  # SGR encoding
            sys.stdout.write("\x1b[?1003h")  # any-event tracking
            sys.stdout.flush()
            self._mouse_active = True
            logger.info("Mouse tracking activated (SGR 1006 + mode 1003)")

    def _detect_kitty_support(self):
        """Probe terminal for Kitty keyboard protocol support.

        Sends the Kitty query sequence CSI ? u and checks for a response.
        Must be called after cbreak mode is active so reads don't block.
        """
        fd = sys.stdin.fileno()

        # Drain any pending input from terminal setup
        while True:
            ready, _, _ = select.select([fd], [], [], 0.01)
            if not ready:
                break
            os.read(fd, 4096)

        # Query current Kitty progressive enhancement level
        sys.stdout.write("\x1b[?u")
        sys.stdout.flush()

        # Wait for response — supported terminals reply with CSI ? <flags> u
        ready, _, _ = select.select([fd], [], [], 0.15)
        if not ready:
            return False

        data = os.read(fd, 1024)
        return b'\x1b[?' in data

    def _cleanup_terminal(self):
        """Exit blessed terminal context managers in reverse order."""
        if self._mouse_active:
            sys.stdout.write("\x1b[?1003l")
            sys.stdout.write("\x1b[?1006l")
            sys.stdout.flush()
            self._mouse_active = False
        if self._kitty_active:
            sys.stdout.write("\x1b[<u")
            sys.stdout.flush()
            self._kitty_active = False
        # Restore cursor visibility before tearing down contexts
        if self._term is not None:
            try:
                print(self._term.normal_cursor, end="", flush=True)
            except Exception:
                pass
        for ctx in reversed(self._term_contexts):
            try:
                ctx.__exit__(None, None, None)
            except Exception:
                pass
        self._term_contexts.clear()
        self._term = None

    def start(self, activity: Activity = None):
        self.setup_logger()

        # If no screen was provided (real terminal mode), create a blessed one
        if self.curses_screen is None:
            self._setup_blessed_terminal()

        CentralDispatch.default_exception_handler = self._shutdown_app_exception_handler

        self.main_thread = CentralDispatch.create_serial_queue()
        self.background_thread = CentralDispatch.create_serial_queue()
        self.subscribe(event_type=ExceptionOccured, activity=self, callback=self.on_exception)
        self.subscribe(event_type=KeyStroke, activity=self, callback=self.on_key_stroke)
        self.shutdown_signal = CentralDispatch.future(self._event_monitor)
        self.start_key_monitor()
        self.on_start()

        if activity:
            self.segue_to(activity)

        shutdown_event = self.shutdown_signal.result()

        self.handle_shutdown(shutdown_event)
        self._cleanup_terminal()

    def on_start(self): pass

    def _stop_activity(self, activity: Activity):
        activity._stop()
        self.unsubscribe_all(activity)

    def _start_activity(self, activity: Activity):
        activity._start(application=self)

    def _segue_to(self, activity: Activity, segue_type):
        if len(self.stack) > 0:
            if segue_type == Segue.REPLACE:
                current_activity = self.stack.pop()
            else:
                current_activity = self.stack[-1]
            self._stop_activity(current_activity)
        self.stack.append(activity)
        self._start_activity(activity)

    def segue_to(self, activity: Activity, segue_type=Segue.PUSH):
        self.main_thread.submit_async(self._segue_to, activity, segue_type=segue_type)

    def _pop_activity(self):
        current_activity = self.stack.pop()
        if len(self.stack) > 0:
            returning_activity = self.stack[-1]

            self._stop_activity(current_activity)
            self._start_activity(returning_activity)
        else:
            self.event_queue.put(StopApplication())

    def pop_activity(self):
        self.main_thread.submit_async(self._pop_activity)

    def _dispatch_event(self, callback, event):
        callback(event)

    def dispatch_event(self, event):
        for labeled_callback in self.event_subscribers[type(event)]:
            self.main_thread.submit_async(self._dispatch_event, labeled_callback.callback, event)

    def _event_monitor(self):
        event = self.event_queue.get()

        while not isinstance(event, StopApplication):
            self.dispatch_event(event)
            event = self.event_queue.get()

        return event

    # Number of consecutive 50ms inkey() timeouts before generating a
    # synthetic KeyRelease.  3 × 50ms = 150ms — snappy taps, small gap
    # at the start of holds on terminals without Kitty protocol.
    _IDLE_RELEASE_TICKS = 3

    # Interval between dispatched repeat events in synth mode.
    # Matches _IDLE_RELEASE_TICKS × 50ms = 150ms so each staccato beep
    # has the same length as the gap before release fires.
    _REPEAT_INTERVAL_S = 0.15

    def _key_monitor_blessed(self, term):
        """Read keys from blessed Terminal, with synthetic KeyRelease."""
        synth = self._enable_kitty and not self._kitty_active
        last_key = None     # last key dispatched (for synthetic release)
        idle_count = 0      # consecutive timeouts since last key event
        last_repeat_time = 0.0  # monotonic time of last dispatched repeat

        while not self.shutdown_signal.done():
            keystroke = term.inkey(timeout=0.05)

            if not keystroke:
                # Timeout — no key data this cycle
                if synth and last_key is not None:
                    idle_count += 1
                    if idle_count >= self._IDLE_RELEASE_TICKS:
                        self.event_queue.put(KeyRelease(last_key))
                        last_key = None
                continue

            code = blessed_key_to_int(keystroke)
            if code is None:
                continue
            if code == 3:  # Ctrl-C
                self.event_queue.put(StopApplication())
                return

            normalized = Keys.normalize(code)

            if synth:
                if normalized != last_key:
                    # New key — release the old one first
                    if last_key is not None:
                        self.event_queue.put(KeyRelease(last_key))
                    self.event_queue.put(KeyStroke(normalized))
                    last_key = normalized
                    last_repeat_time = time.monotonic()
                else:
                    # Same key (OS repeat) — throttle to one event per interval
                    now = time.monotonic()
                    if now - last_repeat_time >= self._REPEAT_INTERVAL_S:
                        self.event_queue.put(
                            KeyStroke(normalized, state=KeyState.REPEATED)
                        )
                        last_repeat_time = now
                    # else: swallow — prevents queue buildup
                # Reset idle counter on any key activity
                idle_count = 0
            else:
                state = blessed_keystroke_state(keystroke)
                if state == KeyState.RELEASED:
                    self.event_queue.put(KeyRelease(normalized))
                else:
                    self.event_queue.put(KeyStroke(normalized, state=state))

    def _key_monitor_kitty(self, term):
        """Read keys using raw Kitty keyboard protocol CSI parsing."""
        fd = sys.stdin.fileno()
        buf = b""
        logger.info("Kitty key monitor started on fd={}", fd)

        while not self.shutdown_signal.done():
            ready, _, _ = select.select([fd], [], [], 0.05)
            if not ready:
                if buf:
                    # Timeout with incomplete buffer — flush as individual bytes
                    # (handles bare ESC, or non-Kitty terminals)
                    logger.debug("Kitty timeout flush: {!r}", buf)
                    for byte in buf:
                        if byte == 3:
                            self.event_queue.put(StopApplication())
                            return
                        self.event_queue.put(KeyStroke(Keys.normalize(byte)))
                    buf = b""
                continue
            data = os.read(fd, 1024)
            if not data:
                continue
            logger.debug("Kitty raw: {!r}", data)
            buf += data
            buf = self._process_kitty_buffer(buf)

    def _process_kitty_buffer(self, buf):
        """Parse all complete Kitty sequences from buffer, dispatch events."""
        while buf:
            if buf[0:1] == b'\x1b':
                if len(buf) < 3:
                    break  # incomplete escape sequence
                if buf[1:2] == b'[':
                    consumed, event = self._parse_csi_sequence(buf)
                    if consumed == 0:
                        break  # incomplete
                    if isinstance(event, StopApplication):
                        self.event_queue.put(event)
                        return b""
                    if event is not None:
                        if isinstance(event, (self._MOUSE_EVENT_TYPES + (_RawMouseMotion,))):
                            for synth in self._synthesize_mouse_events(event):
                                self.event_queue.put(synth)
                        else:
                            if (isinstance(event, KeyStroke)
                                    and event.state == KeyState.REPEATED):
                                now = time.monotonic()
                                if now - self._last_kitty_repeat_time < self._REPEAT_INTERVAL_S:
                                    buf = buf[consumed:]
                                    continue  # swallow — prevents queue buildup
                                self._last_kitty_repeat_time = now
                            self.event_queue.put(event)
                    buf = buf[consumed:]
                else:
                    buf = buf[1:]  # skip unknown ESC sequence byte
            else:
                # Raw byte (shouldn't happen with Kitty disambiguate, but handle it)
                code = buf[0]
                if code == 3:  # Ctrl-C
                    self.event_queue.put(StopApplication())
                    return b""
                self.event_queue.put(KeyStroke(Keys.normalize(code)))
                buf = buf[1:]
        return buf

    def _parse_csi_sequence(self, buf):
        """Parse one CSI sequence starting with ESC [.

        Returns (bytes_consumed, event) or (0, None) if incomplete.
        """
        # SGR mouse sequences start with ESC [ <
        if len(buf) > 2 and buf[2:3] == b'<':
            return self._parse_sgr_mouse(buf)

        for i in range(2, len(buf)):
            if 0x40 <= buf[i] <= 0x7E:  # CSI final byte
                final = chr(buf[i])
                params = buf[2:i].decode('ascii', errors='replace')
                consumed = i + 1
                if final == 'u':
                    return consumed, self._decode_kitty_u(params)
                return consumed, self._decode_kitty_csi(params, final)
        # No final byte found
        if len(buf) > 32:
            return len(buf), None  # discard oversized junk
        return 0, None  # incomplete

    def _decode_kitty_u(self, params):
        """Decode CSI <codepoint>[;<modifiers>[:<event_type>]] u."""
        parts = params.split(';')
        try:
            codepoint = int(parts[0])
        except (ValueError, IndexError):
            return None

        event_type = 1  # press
        modifiers = 0

        if len(parts) > 1:
            mod_parts = parts[1].split(':')
            try:
                modifiers = int(mod_parts[0]) - 1
            except ValueError:
                pass
            if len(mod_parts) > 1:
                try:
                    event_type = int(mod_parts[1])
                except ValueError:
                    pass

        # Ctrl-C: codepoint 'c' (99) with Ctrl modifier (bit 2)
        if codepoint == 99 and (modifiers & 4):
            return StopApplication()
        if codepoint == 3:
            return StopApplication()

        code = Keys.normalize(codepoint)

        if event_type == 3:
            return KeyRelease(code)
        elif event_type == 2:
            return KeyStroke(code, state=KeyState.REPEATED)
        else:
            return KeyStroke(code, state=KeyState.PRESSED)

    _CSI_FINAL_TO_KEY = {
        'A': Keys.UP, 'B': Keys.DOWN, 'C': Keys.RIGHT, 'D': Keys.LEFT,
        'H': Keys.HOME, 'F': Keys.END,
    }

    def _decode_kitty_csi(self, params, final):
        """Decode enhanced CSI sequences for special keys (arrows, etc.)."""
        event_type = 1
        parts = params.split(';')
        if len(parts) > 1:
            mod_parts = parts[1].split(':')
            if len(mod_parts) > 1:
                try:
                    event_type = int(mod_parts[1])
                except ValueError:
                    pass

        code = self._CSI_FINAL_TO_KEY.get(final)
        if code is None and final == '~':
            try:
                num = int(parts[0])
            except (ValueError, IndexError):
                return None
            code = {1: Keys.HOME, 4: Keys.END}.get(num)

        if code is None:
            return None

        if event_type == 3:
            return KeyRelease(code)
        elif event_type == 2:
            return KeyStroke(code, state=KeyState.REPEATED)
        else:
            return KeyStroke(code, state=KeyState.PRESSED)

    def _parse_sgr_mouse(self, buf):
        """Parse an SGR mouse sequence: ESC [ < Cb ; Cx ; Cy M/m.

        Returns (bytes_consumed, event) or (0, None) if incomplete.
        """
        # Find the final byte M (press/motion) or m (release)
        for i in range(3, len(buf)):
            if buf[i] == ord('M') or buf[i] == ord('m'):
                params = buf[3:i].decode('ascii', errors='replace')
                is_release = (buf[i] == ord('m'))
                consumed = i + 1
                parts = params.split(';')
                if len(parts) != 3:
                    return consumed, None
                try:
                    cb = int(parts[0])
                    cx = int(parts[1])
                    cy = int(parts[2])
                except ValueError:
                    return consumed, None
                # Convert from 1-based to 0-based
                return consumed, self._decode_sgr_mouse(cb, cx - 1, cy - 1, is_release)
        # No final byte found
        if len(buf) > 32:
            return len(buf), None
        return 0, None

    def _decode_sgr_mouse(self, cb, x, y, is_release):
        """Decode SGR mouse Cb value into a mouse event.

        Cb bit layout:
        - bits 0-1: button (0=left, 1=middle, 2=right)
        - bit 5 (32): motion flag
        - bit 6 (64): scroll flag
        - bits 2-4: modifier bits (4=shift, 8=meta, 16=ctrl)
        """
        modifiers = cb & 0x1c  # bits 2-4
        is_motion = bool(cb & 32)
        is_scroll = bool(cb & 64)

        if is_scroll:
            # bits 0-1: 0=scroll up, 1=scroll down
            direction = "down" if (cb & 1) else "up"
            return MouseScroll(direction, x, y, modifiers)

        button = cb & 3  # bits 0-1

        if is_motion:
            if button == 3:
                # Motion with no button held
                return MouseMove(x, y, modifiers)
            return _RawMouseMotion(button, x, y, modifiers)

        if is_release:
            return MouseUp(button, x, y, modifiers)

        return MouseDown(button, x, y, modifiers)

    def _synthesize_mouse_events(self, raw_event):
        """Convert raw mouse events into higher-level synthesized events.

        Returns a list of events to dispatch.
        """
        events = []

        if isinstance(raw_event, MouseDown):
            self._mouse_last_down = (raw_event.button, raw_event.x, raw_event.y)
            self._mouse_drag_state = {
                "button": raw_event.button,
                "start_x": raw_event.x,
                "start_y": raw_event.y,
                "dragging": False,
            }
            events.append(raw_event)

        elif isinstance(raw_event, _RawMouseMotion):
            ds = self._mouse_drag_state
            if ds and ds["button"] == raw_event.button:
                ds["dragging"] = True
                events.append(MouseDrag(
                    raw_event.button, raw_event.x, raw_event.y,
                    ds["start_x"], ds["start_y"], raw_event.modifiers,
                ))

        elif isinstance(raw_event, MouseUp):
            ds = self._mouse_drag_state
            if ds and ds["dragging"] and ds["button"] == raw_event.button:
                events.append(MouseDrag(
                    raw_event.button, raw_event.x, raw_event.y,
                    ds["start_x"], ds["start_y"], raw_event.modifiers,
                ))
            events.append(raw_event)
            ld = self._mouse_last_down
            if ld and not (ds and ds["dragging"]):
                if ld[0] == raw_event.button and ld[1] == raw_event.x and ld[2] == raw_event.y:
                    events.append(MouseClick(
                        raw_event.button, raw_event.x, raw_event.y,
                        raw_event.modifiers,
                    ))
            self._mouse_drag_state = None
            self._mouse_last_down = None

        else:
            # MouseScroll, MouseMove — pass through
            events.append(raw_event)

        return events

    _MOUSE_EVENT_TYPES = (MouseDown, MouseUp, MouseScroll, MouseMove, MouseClick, MouseDrag)

    def _key_monitor(self, screen):
        """Read keys from a screen with getch() (MockScreen path)."""
        while not self.shutdown_signal.done():
            key = screen.getch()
            if key == -1:
                continue
            if key == 3:
                self.event_queue.put(StopApplication())
                return
            else:
                self.event_queue.put(KeyStroke(Keys.normalize(key)))

    def start_key_monitor(self):
        # Run on a dedicated thread so it doesn't block background_thread's
        # single worker (which other activities need for async fetches).
        if self._term is not None:
            if self._kitty_active or self._mouse_active:
                CentralDispatch.future(self._key_monitor_kitty, self._term)
            else:
                CentralDispatch.future(self._key_monitor_blessed, self._term)
        else:
            CentralDispatch.future(self._key_monitor, self.curses_screen)

    def _debug_message(self, lines):
        self.curses_screen.clear()
        for index, line in enumerate(lines):
            self.curses_screen.addstr(index, 0, line)

        self.curses_screen.refresh()

    def debug_message(self, message: str):
        lines = message.split("\n")

        self.main_thread.submit_async(self._debug_message, lines)

    def on_key_stroke(self, event: KeyStroke):
        if event.key == Keys.F1:
            self.segue_to(LogViewerActivity())

    def on_exception(self, event: ExceptionOccured):
        if self.last_exception is not None:
            logger.info("While handling one exception, another occurred.\nOriginal exception: {}")
            logger.info(f"{self.last_exception.__class__.__name__}: {self.last_exception}")
            logger.info(traceback.format_exc())
            self.event_queue.put(StopApplication(exception=event.exception))
        else:
            logger.error(f"Exception in user code occurred: {event.exception}")
            self.last_exception = event.exception
            self.segue_to(ShowExceptionActivity(event.exception))

    def _shutdown_app_exception_handler(self, function):
        def inner_function(*args, **kwargs):
            try:
                return function(*args, **kwargs)
            except Exception as e:
                self.event_queue.put(ExceptionOccured(exception=e))

        return inner_function
