import curses
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
from .Service import Service, ServiceState
from . import Keys
from .activities.LogViewerActivity import LogViewerActivity
from .activities.ShowExceptionActivity import ShowExceptionActivity


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


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


class Application:
    def __init__(self, curses_screen):
        self.log_filename = "application.log"
        self.curses_screen = curses_screen
        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

    # -- 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_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 start(self, activity: Activity = None):
        self.setup_logger()
        curses.curs_set(0)
        curses.set_escdelay(25)
        try:
            self.curses_screen.timeout(50)
        except Exception:
            pass
        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)

    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

    def _key_monitor(self, screen):
        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):
        self.background_thread.submit_async(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 == curses.KEY_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
