#!/usr/bin/env python3
"""
Redis-MQTT Bridge Service for NEMO Plugin.

NEMO never connects to the MQTT broker. It publishes events to Redis (see redis_publisher.py).
This bridge is a separate process that:
  1. Connects to Redis (localhost:6379 db=1) and consumes from the events list
  2. Connects to the MQTT broker (using plugin config: host, port, auth)
  3. For each event from Redis, publishes to the broker

So the bridge is the only component that talks to the broker; it forwards Redis → MQTT.
Connection to the broker uses mqtt_connection.connect_mqtt() and honors max_reconnect_attempts
and reconnect_delay from the saved MQTT configuration.

Modes:
  - AUTO: Starts Redis and Mosquitto for development
  - EXTERNAL: Connects to existing services (production)
"""
import json
import logging
import os
import signal
import sys
import threading
import time

import paho.mqtt.client as mqtt
import redis

if __name__ == "__main__":
    sys.path.insert(
        0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
    )
    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings_dev")
    import django

    django.setup()

try:
    from NEMO_mqtt_bridge.models import MQTTConfiguration
    from NEMO_mqtt_bridge.utils import get_mqtt_config
except ImportError:
    from NEMO.plugins.NEMO_mqtt_bridge.models import MQTTConfiguration
    from NEMO.plugins.NEMO_mqtt_bridge.utils import get_mqtt_config

try:
    from NEMO_mqtt_bridge.connection_manager import ConnectionManager
    from NEMO_mqtt_bridge.redis_publisher import (
        EVENTS_LIST_KEY,
        BRIDGE_CONTROL_KEY,
        BRIDGE_STATUS_KEY,
        BRIDGE_STATUS_TTL,
    )
    from NEMO_mqtt_bridge.bridge.process_lock import acquire_lock, release_lock
    from NEMO_mqtt_bridge.bridge.auto_services import (
        cleanup_existing_services,
        start_redis,
        start_mosquitto,
    )
    from NEMO_mqtt_bridge.bridge.mqtt_connection import connect_mqtt
except ImportError:
    from NEMO.plugins.NEMO_mqtt_bridge.connection_manager import ConnectionManager
    from NEMO.plugins.NEMO_mqtt_bridge.redis_publisher import (
        EVENTS_LIST_KEY,
        BRIDGE_CONTROL_KEY,
        BRIDGE_STATUS_KEY,
        BRIDGE_STATUS_TTL,
    )
    from NEMO.plugins.NEMO_mqtt_bridge.bridge.process_lock import acquire_lock, release_lock
    from NEMO.plugins.NEMO_mqtt_bridge.bridge.auto_services import (
        cleanup_existing_services,
        start_redis,
        start_mosquitto,
    )
    from NEMO.plugins.NEMO_mqtt_bridge.bridge.mqtt_connection import connect_mqtt

logger = logging.getLogger(__name__)


class RedisMQTTBridge:
    """Bridges Redis events to MQTT broker."""

    def __init__(self, auto_start: bool = False):
        self.auto_start = auto_start
        self.mqtt_client = None
        self.redis_client = None
        self.running = False
        self.config = None
        self.thread = None
        self.lock_file = None
        self.redis_process = None
        self.mosquitto_process = None
        self.broker_host = None
        self.broker_port = None
        self.connection_count = 0
        self.last_connect_time = None
        self.last_disconnect_time = None
        # Debounce disconnect logging (paho can fire on_disconnect many times)
        self._last_disconnect_log_time = 0
        self._last_disconnect_rc = None
        self._disconnect_log_interval = 5
        # Throttle reconnection failure logs when circuit breaker is open
        self._last_reconnect_fail_log_time = 0
        self._last_reconnect_fail_msg = None
        self._reconnect_fail_log_interval = 30
        self._last_reconnecting_log_time = 0
        self._reconnecting_log_interval = 15
        self._mqtt_has_connected_before = False
        self._last_bridge_status_write = 0  # refresh "connected" in Redis for monitor

        # MQTT connection manager created in _initialize_mqtt() from config (max_retries, reconnect_delay)
        self.mqtt_connection_mgr = None
        self.redis_connection_mgr = ConnectionManager(
            max_retries=None,
            base_delay=1,
            max_delay=30,
            failure_threshold=5,
            success_threshold=3,
            timeout=60,
        )

        self.lock_file = acquire_lock()
        signal.signal(signal.SIGINT, self._signal_handler)
        signal.signal(signal.SIGTERM, self._signal_handler)

    def _signal_handler(self, signum, frame):
        logger.info("Received signal %s, shutting down", signum)
        self.stop()
        sys.exit(0)

    def start(self):
        """Start the bridge service."""
        try:
            mode = "AUTO" if self.auto_start else "EXTERNAL"
            logger.info("Starting Redis-MQTT Bridge (%s mode)", mode)

            self.config = get_mqtt_config()
            if not self.config or not self.config.enabled:
                logger.error("No enabled MQTT configuration found")
                return False

            if self.auto_start:
                cleanup_existing_services(self.redis_process)
                self.redis_process = start_redis()
                self.mosquitto_process = start_mosquitto(self.config)

            self._initialize_redis()
            self._initialize_mqtt()

            self.running = True
            self.thread = threading.Thread(target=self._run, daemon=True)
            self.thread.start()

            logger.info("Redis-MQTT Bridge started successfully")
            return True
        except Exception as e:
            logger.error("Failed to start bridge: %s", e)
            return False

    def _initialize_redis(self):
        def connect():
            c = redis.Redis(
                host="localhost",
                port=6379,
                db=1,
                decode_responses=True,
                socket_connect_timeout=5,
                socket_timeout=5,
            )
            c.ping()
            return c

        self.redis_client = self.redis_connection_mgr.connect_with_retry(connect)
        logger.info("Connected to Redis")

    def _initialize_mqtt(self):
        # Stop existing client so broker can release the session and we don't accumulate clients
        if self.mqtt_client is not None:
            try:
                self.mqtt_client.loop_stop()
                self.mqtt_client.disconnect()
            except Exception as e:
                logger.debug("Cleanup of previous MQTT client: %s", e)
            self.mqtt_client = None

        self.config = get_mqtt_config()
        if not self.config or not self.config.enabled:
            raise RuntimeError("No enabled MQTT configuration")

        # Create connection manager from config so max_retries and reconnect_delay are honored
        max_retries = (
            self.config.max_reconnect_attempts
            if self.config.max_reconnect_attempts
            else None
        )  # 0 = unlimited
        base_delay = getattr(self.config, "reconnect_delay", 5) or 5
        self.mqtt_connection_mgr = ConnectionManager(
            max_retries=max_retries,
            base_delay=base_delay,
            max_delay=60,
            failure_threshold=5,
            success_threshold=3,
            timeout=60,
        )

        def connect():
            self.config = get_mqtt_config()
            if not self.config or not self.config.enabled:
                raise RuntimeError("No enabled MQTT configuration")
            self.broker_host = self.config.broker_host or "localhost"
            self.broker_port = self.config.broker_port or 1883
            return connect_mqtt(
                self.config,
                self._on_connect,
                self._on_disconnect,
                self._on_publish,
            )

        self.mqtt_client = self.mqtt_connection_mgr.connect_with_retry(connect)
        self.connection_count += 1
        self.last_connect_time = time.time()
        self._last_reconnect_fail_msg = None  # Reset so next failure is logged
        logger.info(
            "Connected to MQTT broker at %s:%s", self.broker_host, self.broker_port
        )

    def _on_connect(self, client, userdata, flags, rc):
        if rc == 0:
            self._write_bridge_status("connected")
            if self._mqtt_has_connected_before:
                logger.info(
                    "Successfully reconnected to MQTT broker at %s:%s",
                    self.broker_host,
                    self.broker_port,
                )
            else:
                logger.info(
                    "Connected to MQTT broker at %s:%s",
                    self.broker_host,
                    self.broker_port,
                )
                self._mqtt_has_connected_before = True
        else:
            self._write_bridge_status("disconnected")
            errors = {
                1: "protocol",
                2: "client id",
                3: "unavailable",
                4: "bad auth",
                5: "unauthorized",
            }
            logger.error("MQTT connection failed: %s (rc=%s)", errors.get(rc, rc), rc)

    def _on_disconnect(self, client, userdata, rc):
        self.last_disconnect_time = time.time()
        self._write_bridge_status("disconnected")
        if rc != 0:
            now = time.time()
            rc_changed = self._last_disconnect_rc != rc
            interval_elapsed = (
                now - self._last_disconnect_log_time
            ) >= self._disconnect_log_interval
            if rc_changed or interval_elapsed or self._last_disconnect_log_time == 0:
                logger.warning("MQTT disconnected (rc=%s)", rc)
                self._last_disconnect_log_time = now
                self._last_disconnect_rc = rc

    def _on_publish(self, client, userdata, mid):
        logger.debug("Published mid=%s", mid)

    def _write_bridge_status(self, status: str):
        """Write bridge connection status to Redis for the monitor page."""
        if status not in ("connected", "disconnected"):
            return
        try:
            if self.redis_client:
                self.redis_client.setex(BRIDGE_STATUS_KEY, BRIDGE_STATUS_TTL, status)
        except Exception as e:
            logger.debug("Could not write bridge status to Redis: %s", e)

    def _ensure_mqtt_connected(self):
        if self.mqtt_client and self.mqtt_client.is_connected():
            return True
        now = time.time()
        if (now - self._last_reconnecting_log_time) >= self._reconnecting_log_interval:
            logger.warning("MQTT disconnected, reconnecting...")
            self._last_reconnecting_log_time = now
        try:
            self._initialize_mqtt()
            return True
        except Exception as e:
            msg = str(e)
            should_log = (
                (now - self._last_reconnect_fail_log_time)
                >= self._reconnect_fail_log_interval
                or msg != self._last_reconnect_fail_msg
            )
            if should_log:
                logger.error("Reconnection failed: %s", e)
                self._last_reconnect_fail_log_time = now
                self._last_reconnect_fail_msg = msg
            return False

    def _run(self):
        """Main loop: consume Redis, publish to MQTT."""
        # Honor config log level so DEBUG in NEMO MQTT settings shows HMAC/message debug
        level_name = getattr(self.config, "log_level", None) or "INFO"
        logger.setLevel(getattr(logging, level_name.upper(), logging.INFO))
        logger.info("Starting consumption loop")
        while self.running:
            try:
                if not self._ensure_mqtt_connected():
                    time.sleep(5)
                    continue
                # Refresh "connected" status in Redis so monitor page stays up to date (TTL 90s)
                now = time.time()
                if (now - self._last_bridge_status_write) >= 30:
                    self._write_bridge_status("connected")
                    self._last_bridge_status_write = now
                try:
                    self.redis_client.ping()
                except Exception as e:
                    logger.warning("Redis disconnected: %s", e)
                    self._initialize_redis()
                # Check for config-reload request (e.g. after saving MQTT config in Admin)
                control = self.redis_client.lpop(BRIDGE_CONTROL_KEY)
                if control == "reload_config":
                    logger.info(
                        "Config reload requested, reconnecting to broker with latest settings"
                    )
                    try:
                        from django.core.cache import cache

                        cache.delete("mqtt_active_config")
                    except Exception as e:
                        logger.debug("Could not clear config cache: %s", e)
                    # Force fresh config from DB so broker username/password and HMAC are current
                    self.config = get_mqtt_config(force_refresh=True)
                    # Re-apply log level from new config (e.g. INFO → DEBUG)
                    level_name = getattr(self.config, "log_level", None) or "INFO"
                    logger.setLevel(getattr(logging, level_name.upper(), logging.INFO))
                    self._initialize_mqtt()
                result = self.redis_client.blpop(EVENTS_LIST_KEY, timeout=1)
                if result:
                    channel, event_data = result
                    self._process_event(event_data)
            except Exception as e:
                logger.error("Service loop error: %s", e)
                time.sleep(1)
        logger.info("Consumption loop stopped")

    def _process_event(self, event_data: str):
        try:
            event = json.loads(event_data)
            topic = event.get("topic")
            payload = event.get("payload")
            qos = event.get("qos", 0)
            retain = event.get("retain", False)
            # Debug: exact message from Nemo (Redis) and HMAC secret used for signing
            _secret = (self.config.hmac_secret_key or "") if self.config else ""
            logger.debug(
                "HMAC debug: hmac_secret_key=%r, topic=%s, raw_payload_from_nemo=%r",
                _secret,
                topic,
                payload,
            )
            if topic and payload is not None:
                self._publish_to_mqtt(topic, payload, qos, retain)
                logger.debug(
                    "HMAC debug: hmac_secret_key=%r, topic=%s, published_to_mqtt=ok",
                    _secret,
                    topic,
                )
            else:
                logger.warning("Invalid event: missing topic or payload")
        except json.JSONDecodeError as e:
            logger.error("Invalid JSON: %s", e)
        except Exception as e:
            logger.error("Process event failed: %s", e)

    def _publish_to_mqtt(
        self, topic: str, payload: str, qos: int = 0, retain: bool = False
    ):
        if not self.mqtt_client or not self.mqtt_client.is_connected():
            logger.warning("MQTT not connected, cannot publish")
            return
        _secret = (self.config.hmac_secret_key or "") if self.config else ""
        logger.debug(
            "HMAC debug: hmac_secret_key=%r, topic=%s, payload_before_hmac=%r",
            _secret,
            topic,
            payload,
        )
        try:
            out_payload = payload
            if (
                self.config
                and getattr(self.config, "use_hmac", False)
                and getattr(self.config, "hmac_secret_key", None)
            ):
                try:
                    from NEMO_mqtt_bridge.utils import sign_payload_hmac
                except ImportError:
                    from NEMO.plugins.NEMO_mqtt_bridge.utils import sign_payload_hmac
                try:
                    out_payload = sign_payload_hmac(
                        payload,
                        self.config.hmac_secret_key,
                    )
                    logger.debug(
                        "HMAC debug: hmac_secret_key=%r, topic=%s, exact_mqtt_message_sent=%r",
                        _secret,
                        topic,
                        out_payload,
                    )
                except Exception as e:
                    logger.warning("HMAC signing failed, publishing unsigned: %s", e)
                    logger.debug(
                        "HMAC debug: hmac_secret_key=%r, topic=%s, unsigned_payload_sent=%r",
                        _secret,
                        topic,
                        out_payload,
                    )
            else:
                logger.debug(
                    "HMAC debug: hmac_secret_key=%r, topic=%s, exact_mqtt_message_sent=%r (no HMAC)",
                    _secret,
                    topic,
                    out_payload,
                )
            result = self.mqtt_client.publish(
                topic, out_payload, qos=qos, retain=retain
            )
            if result.rc != mqtt.MQTT_ERR_SUCCESS:
                logger.error("Publish failed: rc=%s", result.rc)
        except Exception as e:
            logger.error("Publish failed: %s", e)

    def stop(self):
        """Stop the bridge service."""
        logger.info("Stopping Redis-MQTT Bridge")
        self.running = False
        if self.mqtt_client:
            self.mqtt_client.loop_stop()
            self.mqtt_client.disconnect()
        if self.redis_client:
            self.redis_client.close()
        if self.thread and self.thread.is_alive():
            self.thread.join(timeout=5)
        if self.auto_start:
            cleanup_existing_services(self.redis_process)
        release_lock(self.lock_file)
        logger.info("Bridge stopped")


_mqtt_bridge_instance = None
_mqtt_bridge_lock = threading.Lock()


def get_mqtt_bridge():
    """Get or create the global bridge instance."""
    global _mqtt_bridge_instance
    with _mqtt_bridge_lock:
        if _mqtt_bridge_instance is None:
            _mqtt_bridge_instance = RedisMQTTBridge(auto_start=True)
        return _mqtt_bridge_instance


def main():
    import argparse

    parser = argparse.ArgumentParser(description="Redis-MQTT Bridge Service")
    parser.add_argument(
        "--auto", action="store_true", help="AUTO mode: start Redis and Mosquitto"
    )
    args = parser.parse_args()

    service = RedisMQTTBridge(auto_start=args.auto)
    try:
        if service.start():
            mode = "AUTO" if args.auto else "EXTERNAL"
            logger.info("Bridge running in %s mode. Ctrl+C to stop.", mode)
            while service.running:
                time.sleep(1)
        else:
            sys.exit(1)
    except KeyboardInterrupt:
        pass
    finally:
        service.stop()


if __name__ == "__main__":
    main()
