from flask_socketio import SocketIO
from flask_babel import gettext

import time
import os
from loguru import logger
from blinker import signal

from changedetectionio import strtobool


class SignalHandler:
    """A standalone class to receive signals"""

    def __init__(self, socketio_instance, datastore):
        self.socketio_instance = socketio_instance
        self.datastore = datastore

        # Connect to the watch_check_update signal
        from changedetectionio.flask_app import watch_check_update as wcc
        wcc.connect(self.handle_signal, weak=False)
        #        logger.info("SignalHandler: Connected to signal from direct import")

        # Connect to the queue_length signal
        queue_length_signal = signal('queue_length')
        queue_length_signal.connect(self.handle_queue_length, weak=False)
        #       logger.info("SignalHandler: Connected to queue_length signal")

        watch_delete_signal = signal('watch_deleted')
        watch_delete_signal.connect(self.handle_deleted_signal, weak=False)

        watch_favicon_bumped_signal = signal('watch_favicon_bump')
        watch_favicon_bumped_signal.connect(self.handle_watch_bumped_favicon_signal, weak=False)

        watch_small_status_comment_signal = signal('watch_small_status_comment')
        watch_small_status_comment_signal.connect(self.handle_watch_small_status_update, weak=False)

        # Connect to the notification_event signal
        notification_event_signal = signal('notification_event')
        notification_event_signal.connect(self.handle_notification_event, weak=False)
        logger.info("SignalHandler: Connected to notification_event signal")

        # One-shot stats refresh for bulk operations that deliberately skip the per-watch
        # signal (see set_last_viewed(send_signal=False)) — n signals would mean n full
        # rescans of every watch plus 3n broadcasts.
        general_stats_signal = signal('general_stats_update')
        general_stats_signal.connect(self.handle_general_stats_update, weak=False)


    def handle_general_stats_update(self, *args, **kwargs):
        """Emit the global counters once, without touching any individual row.

        Sent after a bulk operation. The tab that triggered it is usually reloading anyway;
        this is for OTHER open tabs so their unread/error counters don't sit stale.
        Note their ROWS still keep a stale 'unviewed' class until the row resync lands —
        tracked separately, this only fixes the counters.
        """
        try:
            errored_count = sum(1 for w in self.datastore.data['watching'].values() if w.get('last_error'))
            self.socketio_instance.emit("general_stats_update", {
                'count_errors': errored_count,
                'unread_changes_count': self.datastore.unread_changes_count,
            })
            logger.trace("Socket.IO: Emitted one-shot general_stats_update")
        except Exception as e:
            logger.error(f"Socket.IO error in handle_general_stats_update: {str(e)}")

    def handle_watch_small_status_update(self, *args, **kwargs):
        """Small simple status update, for example 'Connecting...'"""
        watch_uuid = kwargs.get('watch_uuid')
        status = kwargs.get('status')

        if watch_uuid and status:
            logger.debug(f"Socket.IO: Received watch small status update '{status}' for UUID {watch_uuid}")
            # Emit the status update to all connected clients
            self.socketio_instance.emit("watch_small_status_comment", {
                "uuid": watch_uuid,
                "status": status,
                "event_timestamp": time.time()
            })



    def handle_signal(self, *args, **kwargs):
        logger.trace(f"SignalHandler: Signal received with {len(args)} args and {len(kwargs)} kwargs")
        # Safely extract the watch UUID from kwargs
        watch_uuid = kwargs.get('watch_uuid')
        app_context = kwargs.get('app_context')

        if watch_uuid:
            # Get the watch object from the datastore
            watch = self.datastore.data['watching'].get(watch_uuid)
            if watch:
                if app_context:
                    # note
                    with app_context.app_context():
                        with app_context.test_request_context():
                            # Forward to handle_watch_update with the watch parameter
                            handle_watch_update(self.socketio_instance, watch=watch, datastore=self.datastore)
                else:
                    handle_watch_update(self.socketio_instance, watch=watch, datastore=self.datastore)

                logger.trace(f"Signal handler processed watch UUID {watch_uuid}")
            else:
                logger.warning(f"Watch UUID {watch_uuid} not found in datastore")

    def handle_watch_bumped_favicon_signal(self, *args, **kwargs):
        watch_uuid = kwargs.get('watch_uuid')
        if watch_uuid:
            # Emit the queue size to all connected clients
            self.socketio_instance.emit("watch_bumped_favicon", {
                "uuid": watch_uuid,
                "event_timestamp": time.time()
            })
        logger.debug(f"Watch UUID {watch_uuid} got its favicon updated")

    def handle_deleted_signal(self, *args, **kwargs):
        watch_uuid = kwargs.get('watch_uuid')
        if watch_uuid:
            # Emit the queue size to all connected clients
            self.socketio_instance.emit("watch_deleted", {
                "uuid": watch_uuid,
                "event_timestamp": time.time()
            })
        logger.debug(f"Watch UUID {watch_uuid} was deleted")

    def handle_queue_length(self, *args, **kwargs):
        """Handle queue_length signal and emit to all clients"""
        try:
            queue_length = kwargs.get('length', 0)
            logger.debug(f"SignalHandler: Queue length update received: {queue_length}")

            # Emit the queue size to all connected clients
            self.socketio_instance.emit("queue_size", {
                "q_length": queue_length,
                "event_timestamp": time.time()
            })

        except Exception as e:
            logger.error(f"Socket.IO error in handle_queue_length: {str(e)}")

    def handle_notification_event(self, *args, **kwargs):
        """Handle notification_event signal and emit to all clients"""
        try:
            watch_uuid = kwargs.get('watch_uuid')
            logger.debug(f"SignalHandler: Notification event received for watch UUID: {watch_uuid}")

            # Emit the notification event to all connected clients
            self.socketio_instance.emit("notification_event", {
                "watch_uuid": watch_uuid,
                "event_timestamp": time.time()
            })

            logger.trace(f"Socket.IO: Emitted notification_event for watch UUID {watch_uuid}")

        except Exception as e:
            logger.error(f"Socket.IO error in handle_notification_event: {str(e)}")



def handle_watch_update(socketio, **kwargs):
    """Handle watch update signal from blinker"""
    try:
        watch = kwargs.get('watch')
        datastore = kwargs.get('datastore')

        # Emit the watch update to all connected clients
        from changedetectionio.flask_app import update_q
        from changedetectionio.flask_app import _jinja2_filter_datetime, _jinja2_filter_datetimestamp
        from changedetectionio import worker_pool

        # Get list of watches that are currently running
        running_uuids = worker_pool.get_running_uuids()

        # Get list of watches in the queue (efficient single-lock method)
        queue_list = update_q.get_queued_uuids()

        # Get the error texts from the watch
        error_texts = watch.compile_error_texts()
        # Create a simplified watch data object to send to clients

        watch_data = {
            'checking_now': True if watch.get('uuid') in running_uuids else False,
            'error_text': error_texts,
            'event_timestamp': time.time(),
            'fetch_time': watch.get('fetch_time'),
            'has_error': True if error_texts else False,
            'has_favicon': True if watch.get_favicon_filename() else False,
            'history_n': watch.history_n,
            # Uses the same filter as the server-rendered list so the long/short (timeago_format) setting is honoured.
            'last_changed_text': _jinja2_filter_datetimestamp(int(watch.last_changed)) if watch.history_n >= 2 and int(watch.last_changed) > 0 else gettext('Not yet'),
            'last_checked': watch.get('last_checked'),
            'last_checked_text': _jinja2_filter_datetime(watch),
            'notification_muted': True if watch.get('notification_muted') else False,
            'paused': True if watch.get('paused') else False,
            'queued': True if watch.get('uuid') in queue_list else False,
            'unviewed': watch.has_unviewed,
            'uuid': watch.get('uuid'),
        }

        errored_count = 0
        for watch_uuid_iter, watch_iter in datastore.data['watching'].items():
            if watch_iter.get('last_error'):
                errored_count += 1

        general_stats = {
            'count_errors': errored_count,
            'unread_changes_count': datastore.unread_changes_count
        }

        # Debug what's being emitted
        # logger.debug(f"Emitting 'watch_update' event for {watch.get('uuid')}, data: {watch_data}")

        # Emit to all clients (no 'broadcast' parameter needed - it's the default behavior)
        socketio.emit("watch_update", {'watch': watch_data})
        socketio.emit("general_stats_update", general_stats)

        # The set of running UUIDs changes exactly when a worker claims/releases a watch,
        # and this signal fires at both of those moments - so emit the live "checking now" count here.
        socketio.emit("checking_now", {
            "count": len(running_uuids),
            "event_timestamp": time.time()
        })

        # Log after successful emit - use watch_data['uuid'] to avoid variable shadowing issues
        logger.trace(f"Socket.IO: Emitted update for watch {watch_data['uuid']}, Checking now: {watch_data['checking_now']}")

    except Exception as e:
        logger.error(f"Socket.IO error in handle_watch_update: {str(e)}")


def _suppress_werkzeug_ws_abrupt_disconnect_noise():
    """Patch BaseWSGIServer.log to suppress the AssertionError traceback that fires when
    a browser closes a WebSocket connection mid-handshake (e.g. closing a tab).
    The exception is caught inside run_wsgi and routed to self.server.log() — it never
    propagates out, so wrapping run_wsgi doesn't help. Patching the log method is the
    only reliable intercept point. The error is cosmetic: Socket.IO already handles the
    disconnect correctly via its own disconnect handler and timeout logic."""
    try:
        from werkzeug.serving import BaseWSGIServer
        _original_log = BaseWSGIServer.log

        def _filtered_log(self, type, message, *args):
            if type == 'error' and 'write() before start_response' in message:
                return
            _original_log(self, type, message, *args)

        BaseWSGIServer.log = _filtered_log
    except Exception:
        pass


def init_socketio(app, datastore):
    """Initialize SocketIO with the main Flask app"""
    _suppress_werkzeug_ws_abrupt_disconnect_noise()

    import platform
    import sys

    # Platform-specific async_mode selection for better stability
    system = platform.system().lower()
    python_version = sys.version_info

    # Check for SocketIO mode configuration via environment variable
    # Default is 'threading' for best cross-platform compatibility
    socketio_mode = os.getenv('SOCKETIO_MODE', 'threading').lower()

    if socketio_mode == 'gevent':
        # Use gevent mode (higher concurrency but platform limitations)
        try:
            import gevent
            async_mode = 'gevent'
            logger.info(f"SOCKETIO_MODE=gevent: Using {async_mode} mode for Socket.IO")
        except ImportError:
            async_mode = 'threading'
            logger.warning(f"SOCKETIO_MODE=gevent but gevent not available, falling back to {async_mode} mode")
    elif socketio_mode == 'threading':
        # Use threading mode (default - best compatibility)
        async_mode = 'threading'
        logger.info(f"SOCKETIO_MODE=threading: Using {async_mode} mode for Socket.IO")
    else:
        # Invalid mode specified, use default
        async_mode = 'threading'
        logger.warning(f"Invalid SOCKETIO_MODE='{socketio_mode}', using default {async_mode} mode for Socket.IO")

    # Log platform info for debugging
    logger.info(f"Platform: {system}, Python: {python_version.major}.{python_version.minor}, Socket.IO mode: {async_mode}")

    # Restrict SocketIO CORS to same origin by default, can be overridden with env var
    cors_origins = os.environ.get('SOCKETIO_CORS_ORIGINS', None)

    socketio = SocketIO(app,
                        async_mode=async_mode,
                        cors_allowed_origins=cors_origins,  # None means same-origin only
                        logger=strtobool(os.getenv('SOCKETIO_LOGGING', 'False')),
                        engineio_logger=strtobool(os.getenv('SOCKETIO_LOGGING', 'False')),
                        # Disable WebSocket compression to prevent memory accumulation
                        # Flask-Compress already handles HTTP response compression
                        engineio_options={'http_compression': False, 'compression_threshold': 0})

    # Set up event handlers
    logger.info("Socket.IO: Registering connect event handler")

    @socketio.on('checkbox-operation')
    def event_checkbox_operations(data):
        from changedetectionio.blueprint.ui import _handle_operations
        from changedetectionio import queuedWatchMetaData
        from changedetectionio import worker_pool
        from changedetectionio.flask_app import update_q, watch_check_update
        import threading

        logger.trace(f"Got checkbox operations event: {data}")

        datastore = socketio.datastore
        # Capture the requesting client's session id now (request context is gone inside the thread)
        # so we can send the feedback toast only back to them, not broadcast to everyone.
        from flask import request
        sid = getattr(request, 'sid', None)

        def run_operation():
            """Run the operation in a background thread to avoid blocking the socket.io event loop"""
            try:
                result = _handle_operations(
                    op=data.get('op'),
                    uuids=data.get('uuids'),
                    datastore=datastore,
                    extra_data=data.get('extra_data'),
                    worker_pool=worker_pool,
                    update_q=update_q,
                    queuedWatchMetaData=queuedWatchMetaData,
                    watch_check_update=watch_check_update,
                    emit_flash=False
                )
                # Server-driven feedback: tell the requesting client what happened (real count / errors)
                if result and result.get('message') and sid:
                    socketio.emit('toast', {'message': result['message'], 'type': result.get('type', 'success')}, to=sid)
            except Exception as e:
                logger.error(f"Error in checkbox operation thread: {e}")

        # Start operation in a disposable daemon thread
        thread = threading.Thread(target=run_operation, daemon=True, name=f"checkbox-op-{data.get('op')}")
        thread.start()

    @socketio.on('connect')
    def handle_connect():
        """Handle client connection"""
        #        logger.info("Socket.IO: CONNECT HANDLER CALLED - Starting connection process")
        from flask import request
        from flask_login import current_user
        from changedetectionio.flask_app import update_q
        from changedetectionio import worker_pool

        # Access datastore from socketio
        datastore = socketio.datastore
        #        logger.info(f"Socket.IO: Current user authenticated: {current_user.is_authenticated if hasattr(current_user, 'is_authenticated') else 'No current_user'}")

        # Check if authentication is required and user is not authenticated
        has_password_enabled = datastore.data['settings']['application'].get('password') or os.getenv("SALTED_PASS", False)
        #        logger.info(f"Socket.IO: Password enabled: {has_password_enabled}")
        if has_password_enabled and not current_user.is_authenticated:
            logger.warning("Socket.IO: Rejecting unauthenticated connection")
            return False  # Reject the connection

        # Send the current queue size to the newly connected client
        try:
            queue_size = update_q.qsize()
            socketio.emit("queue_size", {
                "q_length": queue_size,
                "event_timestamp": time.time()
            }, room=request.sid)  # Send only to this client
            logger.debug(f"Socket.IO: Sent initial queue size {queue_size} to new client")

            # Send the current "checking now" count (how many watches workers are processing right now)
            checking_now = len(worker_pool.get_running_uuids())
            socketio.emit("checking_now", {
                "count": checking_now,
                "event_timestamp": time.time()
            }, room=request.sid)  # Send only to this client
            logger.debug(f"Socket.IO: Sent initial checking_now {checking_now} to new client")
        except Exception as e:
            logger.error(f"Socket.IO error sending initial queue size: {str(e)}")

        logger.info("Socket.IO: Client connected")

    #    logger.info("Socket.IO: Registering disconnect event handler")
    @socketio.on('disconnect')
    def handle_disconnect():
        """Handle client disconnection"""
        logger.info("Socket.IO: Client disconnected")

    # Create a dedicated signal handler that will receive signals and emit them to clients
    signal_handler = SignalHandler(socketio, datastore)

    # Register watch operation event handlers
    from .events import register_watch_operation_handlers
    register_watch_operation_handlers(socketio, datastore)

    # Store the datastore reference on the socketio object for later use
    socketio.datastore = datastore

    # No stop event needed for threading mode - threads check app.config.exit directly

    # Add a shutdown method to the socketio object
    def shutdown():
        """Shutdown the SocketIO server fast and aggressively"""
        try:
            logger.info("Socket.IO: Fast shutdown initiated...")
            logger.info("Socket.IO: Fast shutdown complete")
        except Exception as e:
            logger.error(f"Socket.IO error during shutdown: {str(e)}")

    # Attach the shutdown method to the socketio object
    socketio.shutdown = shutdown

    logger.info("Socket.IO initialized and attached to main Flask app")
    logger.info(f"Socket.IO: Registered event handlers: {socketio.handlers if hasattr(socketio, 'handlers') else 'No handlers found'}")
    return socketio
