class Discovery(threading.Thread):
    __devices_table = ("devices", ("id TEXT NOT NULL UNIQUE PRIMARY KEY",
                                   "name TEXT NOT NULL", "model TEXT NOT NULL",
                                   "local_credentials TEXT NOT NULL",
                                   "last_seen TEXT NOT NULL"))

    def __init__(self, mqtt_client: MQTTClient,
                 device_sessions: typing.Dict[str, Session]):
        super().__init__(name="discovery", daemon=True)
        self.__mqtt_client = mqtt_client
        self.__device_sessions = device_sessions
        self.__device_pool: typing.Dict[str, Device] = dict()
        self.__publish_flag = False
        self.__lock = threading.Lock()
        self.__local_storage = Storage(conf.Discovery.db_path, "devices",
                                       (Discovery.__devices_table, ))

    def __handle_new_device(self, device_id: str, data: dict):
        try:
            logger.info("adding '{}'".format(device_id))
            del data["last_seen"]
            device = Device(id=device_id, **data)
            self.__mqtt_client.publish(
                topic=mgw_dc.dm.gen_device_topic(conf.Client.id),
                payload=json.dumps(mgw_dc.dm.gen_set_device_msg(device)),
                qos=1)
            self.__device_pool[device_id] = device
        except Exception as ex:
            logger.error("adding '{}' failed - {}".format(device_id, ex))

    def __handle_missing_device(self, device_id: str):
        try:
            logger.info("removing '{}' ...".format(device_id))
            device = self.__device_pool[device_id]
            self.__mqtt_client.publish(
                topic=mgw_dc.dm.gen_device_topic(conf.Client.id),
                payload=json.dumps(mgw_dc.dm.gen_delete_device_msg(device)),
                qos=1)
            del self.__device_pool[device_id]
        except Exception as ex:
            logger.error("removing '{}' failed - {}".format(device_id, ex))

    def __handle_existing_device(self, device_id: str, data: dict):
        try:
            logger.info("updating '{}' ...".format(device_id))
            device = self.__device_pool[device_id]
            if device.name != data["name"]:
                name_bk = device.name
                device.name = data["name"]
                try:
                    self.__mqtt_client.publish(
                        topic=mgw_dc.dm.gen_device_topic(conf.Client.id),
                        payload=json.dumps(
                            mgw_dc.dm.gen_set_device_msg(device)),
                        qos=1)
                except Exception as ex:
                    device.name = name_bk
                    raise ex
            if device.local_credentials != data["local_credentials"]:
                device.local_credentials = data["local_credentials"]
        except Exception as ex:
            logger.error("updating '{}' failed - {}".format(device_id, ex))

    def __refresh_local_storage(self):
        try:
            logger.info("refreshing local storage ...")
            local_devices = to_dict(
                self.__local_storage.read(Discovery.__devices_table[0]), "id")
            remote_devices = get_cloud_devices(*get_cloud_credentials())
            new_devices, missing_devices, existing_devices = diff(
                local_devices, remote_devices)
            if new_devices:
                for device_id in new_devices:
                    logger.info("adding record for '{}' ...".format(device_id))
                    try:
                        self.__local_storage.create(
                            Discovery.__devices_table[0], {
                                "id": device_id,
                                **remote_devices[device_id]
                            })
                    except Exception as ex:
                        logger.error(
                            "adding record for '{}' failed - {}".format(
                                device_id, ex))
            if missing_devices:
                for device_id in missing_devices:
                    try:
                        device_data = self.__local_storage.read(
                            Discovery.__devices_table[0], id=device_id)
                        now = time.time()
                        age = now - float(device_data[0]["last_seen"])
                        if age > conf.Discovery.grace_period:
                            logger.info(
                                "removing record for '{}' due to exceeded grace period ..."
                                .format(device_id))
                            try:
                                self.__local_storage.delete(
                                    Discovery.__devices_table[0], id=device_id)
                            except Exception as ex:
                                logger.error(
                                    "removing record for '{}' failed - {}".
                                    format(device_id, ex))
                        else:
                            logger.info(
                                "remaining grace period for missing '{}': {}s".
                                format(device_id,
                                       conf.Discovery.grace_period - age))
                    except Exception as ex:
                        logger.error(
                            "can't calculate grace period for missing '{}' - {}"
                            .format(device_id, ex))
            if existing_devices:
                for device_id in existing_devices:
                    logger.info(
                        "updating record for '{}' ...".format(device_id))
                    try:
                        self.__local_storage.update(
                            Discovery.__devices_table[0],
                            remote_devices[device_id],
                            id=device_id)
                    except Exception as ex:
                        logger.error(
                            "updating record for '{}' failed - {}".format(
                                device_id, ex))
        except Exception as ex:
            logger.error("refreshing local storage failed - {}".format(ex))

    def __refresh_devices(self):
        try:
            stored_devices = to_dict(
                self.__local_storage.read(Discovery.__devices_table[0]), "id")
            new_devices, missing_devices, existing_devices = diff(
                self.__device_pool, stored_devices)
            if new_devices:
                for device_id in new_devices:
                    self.__handle_new_device(device_id,
                                             stored_devices[device_id])
            if missing_devices:
                for device_id in missing_devices:
                    self.__handle_missing_device(device_id)
            if existing_devices:
                for device_id in existing_devices:
                    self.__handle_existing_device(device_id,
                                                  stored_devices[device_id])
        except Exception as ex:
            logger.error("refreshing devices failed - {}".format(ex))

    def __start_device_session(self, device: Device, location: tuple):
        logger.info("found '{}' at '{}'".format(device.id, location[0]))
        session = Session(mqtt_client=self.__mqtt_client,
                          device=device,
                          ip=location[0],
                          port=location[1])
        session.start()
        self.__device_sessions[device.id] = session

    def run(self) -> None:
        if not self.__mqtt_client.connected():
            time.sleep(3)
        logger.info("starting {} ...".format(self.name))
        self.__refresh_local_storage()
        last_cloud_check = time.time()
        self.__refresh_devices()
        while True:
            if self.__publish_flag:
                self.__publish_devices(self.__publish_flag)
            if time.time() - last_cloud_check > conf.Discovery.cloud_delay:
                self.__refresh_local_storage()
                last_cloud_check = time.time()
                self.__refresh_devices()
            try:
                positive_hosts = probe_hosts(discover_hosts())
                for device in self.__device_pool.values():
                    if device.id not in self.__device_sessions:
                        for hostname, data in positive_hosts.items():
                            if device.id.replace(
                                    conf.Discovery.device_id_prefix,
                                    "") in hostname:
                                self.__start_device_session(device=device,
                                                            location=data)
                                break
                    else:
                        if not self.__device_sessions[device.id].is_alive():
                            del self.__device_sessions[device.id]
                            for hostname, data in positive_hosts.items():
                                if device.id.replace(
                                        conf.Discovery.device_id_prefix,
                                        "") in hostname:
                                    self.__start_device_session(device=device,
                                                                location=data)
                                    break
            except Exception as ex:
                logger.error("discovery failed - {}".format(ex))
            time.sleep(conf.Discovery.delay)

    def __publish_devices(self, flag: int):
        with self.__lock:
            if self.__publish_flag == flag:
                self.__publish_flag = 0
        for device in self.__device_pool.values():
            try:
                self.__mqtt_client.publish(
                    topic=mgw_dc.dm.gen_device_topic(conf.Client.id),
                    payload=json.dumps(mgw_dc.dm.gen_set_device_msg(device)),
                    qos=1)
            except Exception as ex:
                logger.error("setting device '{}' failed - {}".format(
                    device.id, ex))
            if flag > 1 and device.state == mgw_dc.dm.device_state.online:
                try:
                    self.__mqtt_client.subscribe(
                        topic=mgw_dc.com.gen_command_topic(device.id), qos=1)
                except Exception as ex:
                    logger.error("subscribing device '{}' failed - {}".format(
                        device.id, ex))

    def schedule_publish(self, subscribe: bool = False):
        with self.__lock:
            self.__publish_flag = max(self.__publish_flag, int(subscribe) + 1)