import logging
import json

import Utils.files as files
import Utils.dialogs as dialogs
import Utils.config as config
from Model.MQTTClient import MQTTClient
from Model.ESP32Cam import ESP32Cam
from Model.Resolutions import ESP32Resolution
from Utils.validator import valid_fields
from Model.MQTTObserver import MQTTObserver
from Model.VideoRecorder import VideoRecorder
from View.MainView import MainView
from View.ConfigView import ConfigView
from View.LoadingPopup import LoadingPopup
from View.ControlView import ControlView 


class Controller(MQTTObserver):

    def __init__(self, main_view: MainView, config_view: ConfigView) -> None:
        self._main_view = main_view
        self._config_view = config_view
        self._loading_view = None
        self._mqtt_client = None
        self._cameras: list[ESP32Cam] = []
        self._streaming = False
        self._connection_tries = 0 
        self._video_recorder = VideoRecorder()
        self._setup_callbacks()
        self._load_saved_config()
        self._config_view.show()
    
       
    def _setup_callbacks(self) -> None:
        # << -- Main View Callbacks -- >>
        self._main_view.frame_timer.timeout.connect(self._main_view.update_frames) # Timer to update the frames
        self._main_view.ping_timer.timeout.connect(self._send_ping_to_cameras) # Timer to ping the cameras to connect them automatically
        self._main_view.timout_timer.timeout.connect(self._handle_camera_configuration_timeout) # Timer to handle timeout to configure the cameras
        self._main_view.start_stop_stream_button.clicked.connect(self._toggle_stream)
        self._main_view.camera_settings_panel.camera_settings_button.clicked.connect(self._configure_camera)
        self._main_view.new_camera_added_signal.connect(self._main_view._update_camera_grid) 
        self._main_view.control_panel_button.clicked.connect(self._show_control_view) # Signal to handle camera clicked

        # << -- Config View Callbacks -- >>
        self._config_view.mqtt_connected.connect(self._handle_mqtt_connection) # Signal to notify when the MQTT client is connected
        self._config_view.connect_button.clicked.connect(self._on_connect_button_click)
        self._config_view.mqtt_not_connected.connect(self._handle_mqtt_not_connected) # Signal to notify when the MQTT client is not connected
    

    def _load_saved_config(self) -> None:
        """Load the saved configuration from the config file."""
        try:
            config_data = config.get_config()
            self._config_view.mqtt_group.broker_ip = config_data["broker_ip"]
            self._config_view.mqtt_group.broker_port = config_data["broker_port"]
            if config_data["broker_username"]:
                self._config_view.mqtt_group.username = config_data["broker_username"]
                self._config_view.mqtt_group.use_credentials = True
            if config_data["mqtt_cert_path"]:
                self._config_view.mqtt_group.tls_path = config_data["mqtt_cert_path"]
                self._config_view.mqtt_group.use_tls = True
            self._config_view.remember_settings_checkbox.setChecked(True)
        except FileNotFoundError:
            return

        

    # << -- Streaming Methods -- >>
    def _toggle_stream(self) -> None:
        """Start or stop the stream when the button is clicked."""
        if self._streaming:
            self._stop_stream()
        else:
            self._start_stream()

    def _start_stream(self) -> None:
        self._streaming = True
        self._main_view.frame_timer.start(1000 // 30) # Start the timer to update frames in the grid (30 FPS)
        for camera in self._cameras: # Start the stream for each camera
            camera.start_stream()
        self._main_view.start_stop_stream_button.setStyleSheet("background-color: 'red';")
        self._main_view.start_stop_stream_button.setText("Stop stream")
        self._main_view.control_panel_button.setHidden(False) # Show the camera control button
        logging.info("Streaming started")

    def _stop_stream(self) -> None:
        self._streaming = False
        self._main_view.frame_timer.stop() # Stop the timer to update frames in the grid 
        for camera in self._cameras: # Stop the stream for each camera
            camera.stop_stream()
        self._main_view.start_stop_stream_button.setStyleSheet(f"background-color: 'green';")
        self._main_view.start_stop_stream_button.setText("Start stream")
        self._main_view.set_default_images() # Set the default images for each camera
        self._main_view.control_panel_button.setHidden(True) # Hide the camera control button
        logging.info("Streaming stopped")
        
    # << -- View Methods -- >>
    def _show_control_view(self) -> None:
        if self._streaming:
            dark_mode = self._main_view.mode == "dark"
            self._control_view = ControlView({camera.id : self._main_view.get_scene_by_camera_id(camera.id) for camera in self._cameras}, self._main_view, dark_mode)
            self._control_view.capture_viewer.take_picture_button.clicked.connect(self._take_picture)
            self._control_view.capture_viewer.start_stop_recording_button.clicked.connect(self._toggle_recording)
            self._control_view.control_panel.add_notification_button.clicked.connect(self._add_notification)
            self._control_view.control_panel.remove_notification_button.clicked.connect(self._remove_notification)
            self._control_view.show()
            self._mqtt_client.publish("telegram/ping", "ping")

    def _remove_notification(self) -> None:
        camera_id = self._control_view.get_selected_camera_id()
        chat_id = self._control_view.control_panel.get_selected_chat_id()
        data = {"camera_id": camera_id, "chat_id": chat_id}
        self._mqtt_client.publish(f"telegram/remove", json.dumps(data)) 
        self._loading_view = LoadingPopup("Removing notification...", self._control_view)
        self._loading_view.show()
        self._main_view.timout_timer.start(5000)


    def _add_notification(self) -> None:
        camera_id = self._control_view.get_selected_camera_id()
        chat_id = self._control_view.control_panel.get_chat_id()
        if not chat_id:
            dialogs.display_error_dialog(self._control_view, "Please enter a chat ID.")
            return

        if camera_id in self._control_view.notifiers:
            if chat_id in self._control_view.notifiers[camera_id]:
                dialogs.display_error_dialog(self._control_view, "Chat ID already added.")
                return
        
            if len(self._control_view.notifiers[camera_id]) == 5:
                dialogs.display_error_dialog(self._control_view, "Maximum number of notifications reached (5).\nPlease remove some notifications.")
                return

        data = {"camera_id": camera_id, "chat_id": chat_id}
        self._mqtt_client.publish(f"telegram/add", json.dumps(data)) 
        self._loading_view = LoadingPopup("Adding notification...", self._control_view)
        self._loading_view.show()
        self._main_view.timout_timer.start(5000) # Start the timer to handle timout of notification to 10 seconds


    # << -- MQTT Methods -- >>
    def _handle_mqtt_connection(self) -> None:
        dialogs.display_info_dialog(self._config_view, "Connected to MQTT broker")
        self._loading_view = LoadingPopup("Detecting cameras...", self._config_view)
        self._loading_view.show()
        self._mqtt_client.subscribe("esp32/pong") 
        self._mqtt_client.subscribe("telegram/pong")
        self._mqtt_client.subscribe_observer(self, "esp32/pong")
        self._mqtt_client.subscribe_observer(self, "telegram/pong")
        self._send_ping_to_cameras()
        self._main_view.ping_timer.start(5000) # Start the timer to ping the cameras every 5 
        
    def _send_ping_to_cameras(self) -> None:
        self._mqtt_client.publish("esp32/ping", "ping") # Send a ping to the cameras to detect them
        if not self._cameras:
            self._connection_tries += 1
            if self._connection_tries == 5:
                self._main_view.ping_timer.stop()
                self._loading_view.close_signal.emit() # Emit the signal to close the loading screen
                dialogs.display_error_dialog(self._loading_view, "No cameras detected.\nPlease check the connection and try again.")

    # Override the update method from MQTTObserver
    def update(self, message):
        """Handle the messages received from the MQTT broker."""
        if message.topic == "esp32/pong": 
            if self._loading_view.isVisible() and not self._cameras:
                self._loading_view.close_signal.emit() # Emit the signal to close the loading screen
                self._config_view.close_signal.emit() # Emit the signal to close the config view
                self._main_view.show_signal.emit() # Show the main view
            
            settings = json.loads(message.payload.decode())
            camera_id = settings["camera_id"]
            if not any(camera.id == camera_id for camera in self._cameras): # Check if the camera is already in the list
                if len(self._cameras) == 12:
                    dialogs.display_error_dialog(self._main_view, "Maximum number of cameras reached (12).\nPlease disconnect some camera.")
                    exit()
                if self._streaming:
                    self._stop_stream() # Stop the stream if it was enabled
                camera = ESP32Cam(camera_id)
                self._set_camera_settings(camera, settings) 
                self._cameras.append(camera) # Add the camera to the list
                topic = "esp32/" + camera_id + "/image"
                self._mqtt_client.subscribe_observer(camera, topic) # Subscribe the camera to its own topic to receive images
                self._mqtt_client.subscribe(topic) # Subscribe the mqtt client to the camera topic
                topic = "esp32/" + camera_id + "/ok"
                self._mqtt_client.subscribe_observer(self, topic) # Subscribe the controller to the camera topic to receive the ok message
                self._mqtt_client.subscribe(topic)
                self._main_view.new_camera_added_signal.emit(self._cameras) # Emit the signal to update the main view
        elif message.topic == "telegram/pong":
            logging.info("Received pong from telegram")
            self._mqtt_client.subscribe("telegram/ok") # Subscribe the mqtt client to the telegram topic
            self._mqtt_client.subscribe_observer(self, "telegram/ok") # Subscribe the controller to the ping topic to receive the ping message

            data = dict(json.loads(message.payload.decode()))
            notifiers = {key: value for key, value in data.items() if any(camera.id == key for camera in self._cameras)}
            self._control_view.notifiers = notifiers # Set the notifiers in the control view
            self._control_view.update_view()
            self._control_view.control_panel.show_telegram_notifications() # Show the telegram notifications section

        elif message.topic == "telegram/ok":
            self._main_view.timout_timer.stop() # Stop the timer
            self._loading_view.close_signal.emit() # Emit the signal to close the loading screen
            data = dict(json.loads(message.payload.decode()))

            if data["action"] == "add":
                camera_id = data["camera_id"]
                chat_id = data["chat_id"]
                self._control_view.add_chat_id(camera_id, chat_id)
                self._control_view.update_view()
            elif data["action"] == "remove":
                camera_id = data["camera_id"]
                chat_id = data["chat_id"]
                self._control_view.remove_chat_id(camera_id, chat_id)
                self._control_view.update_view()
        else: # Camera OK
            self._main_view.timout_timer.stop() # Stop the timer
            self._loading_view.close_signal.emit() # Emit the signal to close the loading screen
    

    # << -- Recording Methods -- >>
    def _toggle_recording(self) -> None:
        """Start or stop the recording when the button is clicked."""
        if self._video_recorder.is_recording():
            self._stop_recording()
        else:
            self._start_recording()    
                
    def _stop_recording(self) -> None:
        self._video_recorder.stop_recording()
        self._control_view.capture_viewer.start_stop_recording_button.setText("Start Recording")
        self._control_view.capture_viewer.start_stop_recording_button.setStyleSheet("background-color: green;")
        dialogs.display_info_dialog(self._control_view, "Video saved successfully.")
        self._control_view.control_panel.setEnabled(True)

    def _start_recording(self) -> None:
        camera_id = self._control_view.get_selected_camera_id()
        camera = self._get_camera(camera_id)
        file_path = files.get_file_path("avi", "Save Video", "Videos (*.avi)", self._control_view)
        if not file_path:
            return
        self._video_recorder.start_recording(file_path, self._control_view.get_current_scene(), camera.resolution.value, camera.get_frame) # Start the recording
        self._control_view.capture_viewer.start_stop_recording_button.setText("Stop Recording")
        self._control_view.capture_viewer.start_stop_recording_button.setStyleSheet("background-color: red;")
        self._control_view.control_panel.setEnabled(False) 
    

    def _take_picture(self) -> None:
        """Take a picture from the camera."""
        camera_id = self._control_view.get_selected_camera_id()
        camera = self._get_camera(camera_id)
        frame = camera.get_frame()
        file_path = files.get_file_path("jpg", "Save Image", "Images (*.jpg *.jpeg)", self._control_view)
        if file_path:
            try:
                files.save_image(file_path, frame)
                dialogs.display_info_dialog(self._control_view, "Image saved successfully.")
            except PermissionError as e:
                dialogs.display_error_dialog(self._control_view, "You don't have permission to save the image in this directory.\nPlease choose another directory.")

    
    def _handle_camera_configuration_timeout(self) -> None:
        """Show error message if the camera is not configured in 10 seconds."""
        self._main_view.timout_timer.stop() # Stop the timer to avoid multiple messages
        dialogs.display_error_dialog(self._loading_view, "Timeout.\nPlease check the connection and try again.")
        self._loading_view.close_signal.emit() # Emit the signal to close the loading screen


    def _get_camera(self, camera_id: str) -> ESP32Cam:
        """Get the camera object from the list of cameras."""
        for camera in self._cameras:
            if camera.id == camera_id:
                return camera
        return None

    def _configure_camera(self) -> None:
        """Update the camera configuration when the button is clicked."""
        self._loading_view = LoadingPopup("Applying configuration...", self._main_view)
        self._loading_view.show()
        self._main_view.timout_timer.start(10000) # Start the timer to handle timeout of camera configuration to 10 seconds
        settings = self._main_view.get_camera_settings() # Get the camera settings from the main view
        camera_id = settings["camera_id"]
        camera = self._get_camera(camera_id)
        self._set_camera_settings(camera, settings) # Set the camera settings
        camera.apply_configuration() # Apply the configuration settings sending it to the camera

    def _set_camera_settings(self, camera: ESP32Cam, settings: dict) -> None:
        """Set the camera settings from the dictionary."""
        camera.resolution = ESP32Resolution.from_string(settings["frame_size"])
        camera.brightness = settings["brightness"]
        camera.contrast = settings["contrast"]
        camera.saturation = settings["saturation"]
        camera.automatic_exposure = settings["auto_exposure"]
        camera.exposure = settings["exposure_value"]
        camera.automatic_gain = settings["auto_gain"]
        camera.gain = settings["gain_value"]
        camera.vertical_flip = settings["vertical_flip"]
        camera.horizontal_flip = settings["horizontal_flip"]
        camera.flash_led = settings["led_on"]

    def _on_connect_button_click(self) -> None:
        broker_ip = self._config_view.mqtt_group.broker_ip
        broker_port = self._config_view.mqtt_group.broker_port
        broker_username = self._config_view.mqtt_group.username
        broker_password = self._config_view.mqtt_group.password
        mqtt_tls_path = self._config_view.mqtt_group.tls_path
        use_credentials = self._config_view.mqtt_group.use_credentials
        use_tls = self._config_view.mqtt_group.use_tls

        valid, message = valid_fields(broker_ip, broker_port, broker_username, broker_password, use_credentials, mqtt_tls_path, use_tls)
        if valid:
            if self._config_view.remember_settings_checkbox.isChecked():
                config.save_config(broker_ip, broker_port, broker_username, mqtt_tls_path)
            else:
                config.delete_config()
            try:
                self._connect_mqtt(broker_ip, broker_port, broker_username, broker_password, mqtt_tls_path)
            except Exception as e:
                dialogs.display_error_dialog(self._config_view, f"Failed to connect to MQTT broker: {e}") 
        else:
            dialogs.display_error_dialog(self._config_view, message)


    def _connect_mqtt(self, ip: str, port: int, username: str, password: str, mqtt_tls_path: str) -> None:
        if self._mqtt_client:
            self._mqtt_client.delete_instance()
            
        self._mqtt_client = MQTTClient()
        self._mqtt_client.set_on_connect(self._on_mqtt_connection)
        self._mqtt_client.connect(ip, port, username, password, mqtt_tls_path)


    def _on_mqtt_connection(self, client, userdata, flags, rc) -> None:
        if rc == 0:
            logging.info("Connected to MQTT broker")
            self._config_view.mqtt_connected.emit() 
        else:
            logging.error(f"Failed to connect to MQTT broker with error code {rc}")
            self._mqtt_client.loop_stop() 
            self._config_view.mqtt_not_connected.emit("Failed to connect to MQTT broker.\nPlease check the connection and try again.")
            

    def _handle_mqtt_not_connected(self, message: str) -> None:
        dialogs.display_error_dialog(self._config_view, message)

