import json
import logging
import platform
import requests
import time
import urllib.parse
import websocket

from threading import Thread

from odoo.addons.iot_drivers import main
from odoo.addons.iot_drivers.tools import helpers, upgrade
from odoo.addons.iot_drivers.server_logger import close_server_log_sender_handler
from odoo.addons.iot_drivers.webrtc_client import webrtc_client

_logger = logging.getLogger(__name__)
websocket.enableTrace(True, level=logging.getLevelName(_logger.getEffectiveLevel()))


@helpers.require_db
def send_to_controller(params, method="send_websocket", server_url=None):
    """Confirm the operation's completion by sending a response back to the Odoo server

    :param params: the parameters to send back to the server
    :param method: method to call on the IoT box controller
    :param server_url: URL of the Odoo server (provided by decorator).
    """
    request_path = f"{server_url}/iot/box/{method}"
    try:
        response = requests.post(request_path, json={'params': params}, timeout=5)
        response.raise_for_status()
    except requests.exceptions.RequestException:
        _logger.exception('Could not reach database URL: %s', request_path)


def on_error(ws, error):
    _logger.error("websocket received an error: %s", error)


@helpers.require_db
class WebsocketClient(Thread):
    channel = ""

    def on_open(self, ws):
        """
            When the client is setup, this function send a message to subscribe to the iot websocket channel
        """
        self.connect_timestamp = time.monotonic()
        ws.send(json.dumps({
            'event_name': 'subscribe',
            'data': {
                'channels': [self.channel],
                'last': self.last_message_id,
                'identifier': helpers.get_identifier(),
            }
        }))

    def on_message(self, ws, messages):
        """Synchronously handle messages received by the websocket."""
        for message in json.loads(messages):
            self.last_message_id = message['id']
            payload = message['message']['payload']
            _logger.info("Received message of type %s", message['message']['type'])

            if not helpers.get_identifier() in payload.get('iot_identifiers', []):
                continue

            match message['message']['type']:
                case 'iot_action':
                    for device_identifier in payload['device_identifiers']:
                        if device_identifier in main.iot_devices:
                            start_operation_time = time.perf_counter()
                            _logger.info("device '%s' action started", device_identifier)
                            main.iot_devices[device_identifier].action(payload)
                            _logger.info("device '%s' action finished - %.*f", device_identifier, 3, time.perf_counter() - start_operation_time)
                        else:
                            # Notify the controller that the device is not connected
                            send_to_controller({
                                'session_id': payload.get('session_id', '0'),
                                'iot_box_identifier': helpers.get_identifier(),
                                'device_identifier': device_identifier,
                                'status': 'disconnected',
                            })
                case 'server_clear':
                    if time.monotonic() < self.connect_timestamp + 5.0:
                        # This is a hacky way avoid processing an old server_clear message
                        # In master we can fix this properly by providing the last message ID to the IoT box on connection
                        _logger.warning("Ignoring server_clear message")
                        continue
                    helpers.disconnect_from_server()
                    close_server_log_sender_handler()
                case 'server_update':
                    helpers.update_conf({
                        'remote_server': payload['server_url']
                    })
                    helpers.get_odoo_server_url.cache_clear()
                case 'restart_odoo':
                    send_to_controller({
                        'session_id': payload['session_id'],
                        'iot_box_identifier': helpers.get_identifier(),
                        'device_identifier': helpers.get_identifier(),
                        'status': 'success',
                    })
                    ws.close()
                    helpers.odoo_restart()
                case 'webrtc_offer':
                    if not webrtc_client:
                        continue
                    answer = webrtc_client.offer(payload['offer'])
                    send_to_controller({
                        'iot_box_identifier': helpers.get_identifier(),
                        'answer': answer,
                    }, method="webrtc_answer")
                case 'remote_debug':
                    if platform.system() == 'Windows':
                        continue
                    if not payload.get("status"):
                        helpers.toggle_remote_connection(payload.get("token", ""))
                        time.sleep(1)
                    send_to_controller({
                        'session_id': 0,
                        'iot_box_identifier': helpers.get_identifier(),
                        'device_identifier': None,
                        'status': 'success',
                        'result': {'enabled': helpers.is_ngrok_enabled()}
                    })
                case "test_connection":
                    send_to_controller({
                        'session_id': payload['session_id'],
                        'iot_box_identifier': helpers.get_identifier(),
                        'device_identifier': helpers.get_identifier(),
                        'status': 'success',
                        'result': {
                            'lan_quality': helpers.check_network(),
                            'wan_quality': helpers.check_network("www.odoo.com"),
                        }
                    })
                case 'bundle_changed':
                    # This message is sent by the DB whenever the web JS asset bundle changes.
                    # While this is a bit of a hack we use this message to check if the DB has been upgraded,
                    # since we know the bundle will always change in this situation.
                    upgrade.check_git_branch()
                case _:
                    continue

    def on_close(self, ws, close_status_code, close_msg):
        _logger.debug("websocket closed with status: %s", close_status_code)
        helpers.update_conf({'last_websocket_message_id': self.last_message_id})

    def __init__(self, channel, server_url=None):
        """This class will not be instantiated if no db is connected.

        :param str channel: the channel to subscribe to
        :param str server_url: URL of the Odoo server (provided by decorator).
        """
        self.channel = channel
        self.last_message_id = int(helpers.get_conf('last_websocket_message_id') or 0)
        self.server_url = server_url
        url_parsed = urllib.parse.urlsplit(server_url)
        scheme = url_parsed.scheme.replace("http", "ws", 1)
        self.websocket_url = urllib.parse.urlunsplit((scheme, url_parsed.netloc, 'websocket', '', ''))
        self.db_name = helpers.get_conf('db_name') or ''
        self.session_id = ''
        super().__init__(daemon=True)

    def run(self):
        if self.db_name:
            session_response = requests.get(
                self.server_url + "/web/login?db=" + self.db_name,
                allow_redirects=False,
                timeout=10,
            )
            if session_response.status_code in [200, 302]:
                self.session_id = session_response.cookies['session_id']
            else:
                _logger.error("Failed to get session ID, status %s", session_response.status_code)

        self.ws = websocket.WebSocketApp(self.websocket_url,
            header={"User-Agent": "OdooIoTBox/1.0", "Cookie": f"session_id={self.session_id}"},
            on_open=self.on_open, on_message=self.on_message,
            on_error=on_error, on_close=self.on_close)

        # The IoT synchronised servers can stop in 2 ways that we need to handle:
        #  A. Gracefully:
        #   In this case a disconnection signal is sent to the IoT-box
        #   The websocket is properly closed, but it needs to be established a new connection when
        #   the server will be back.
        #
        # B. Forced/killed:
        #   In this case there is no disconnection signal received
        #
        #   This will also happen with the graceful quit as `reconnect` will trigger if the server
        #   is offline while attempting the new connection
        while True:
            try:
                run_res = self.ws.run_forever(reconnect=10)
                _logger.debug("websocket run_forever return with %s", run_res)
            except Exception:
                _logger.exception("An unexpected exception happened when running the websocket")
            _logger.debug('websocket will try to restart in 10 seconds')
            time.sleep(10)
