import json
import aiohttp
import asyncio
import async_timeout
import logging
import traceback

from homeassistant.const import EVENT_STATE_CHANGED, ATTR_ENTITY_ID
from homeassistant.core import callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession

from .const import HAVCS_SERVICE_URL, DATA_HAVCS_HANDLER, DATA_HAVCS_BIND_MANAGER, STORAGE_VERSION, INTEGRATION
from . import util as havcs_util

_LOGGER = logging.getLogger(__name__)
LOGGER_NAME = 'bind'

STORAGE_KEY='havcs_bind_manager'
STORAGE_VERSION

# 用于管理哪些平台哪些用户有哪些设备
class HavcsBindManager:
    _privious_upload_devices = {}
    _new_upload_devices = {}
    _discovery = set()
    def __init__(self, hass, platforms, bind_device = False, sync_device = False, app_key = None, decrypt_key = None):
        _LOGGER.debug("[bindManager] ----init bindManager----")
        self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
        self._platforms = platforms
        for platform in platforms:
            self._new_upload_devices[platform] = {}
        self._hass = hass
        self._sync_manager = {
            'bind_device': bind_device,
            'sync_device': sync_device,
            'app_key': app_key,
            'decrypt_key': decrypt_key
        }
    async def async_init(self):
        await self.async_load()
        if self._sync_manager.get('bind_device'):
            await self.async_bind_device()
        if self._sync_manager.get('sync_device'):
            self.sync_device()

    async def async_load(self):
        data =  await self._store.async_load()  # load task config from disk
        if data:
            self._privious_upload_devices = {
                device['device_id']: {'device_id': device['device_id'], 'linked_account': set(device['linked_account'])} for device in data.get('upload_devices',[])
            }
            self._discovery = set(data.get('discovery',[]))
            _LOGGER.debug("[bindManager] discovery:\n%s", self.discovery)

    def get_bind_entity_ids(self, platform, p_user_id = '', repeat_upload = True):
        _LOGGER.debug("[bindManager] privious_upload_devices:\n%s", self._privious_upload_devices)
        _LOGGER.debug("[bindManager] new_upload_devices:\n%s", self._new_upload_devices.get(platform))
        search = set([p_user_id + '@' + platform, '*@' + platform]) # @jdwhale获取平台所有设备,*@jdwhale表示该不限定用户
        if repeat_upload:
            bind_entity_ids = [device['device_id'] for device in self._new_upload_devices.get(platform).values() if search & device['linked_account'] ]
        else:
            bind_entity_ids = [device['device_id'] for device in self._new_upload_devices.get(platform).values() if (search & device['linked_account']) and not(search & self._privious_upload_devices.get(device['device_id'],{}).get('linked_account',set()))]
        return bind_entity_ids
    
    def get_unbind_entity_ids(self, platform, p_user_id = ''):
        search = set([p_user_id + '@' + platform, '*@' + platform])
        unbind_devices = [device['device_id'] for device in self._privious_upload_devices.values() if (search & device['linked_account']) and not(search & self._new_upload_devices.get(platform).get(device['device_id'],{}).get('linked_account',set()))]
        return unbind_devices

    def update_lists(self, devices, platform, p_user_id= '*',repeat_upload = True):
        if platform is None:
            platforms = [platform for platform in self._platforms]
        else:
            platforms = [platform]

        linked_account = set([p_user_id + '@' + platform for platform in platforms])
        # _LOGGER.debug("[bindManager]  0.linked_account:%s", linked_account)
        for device_id in devices:
            if device_id in self._new_upload_devices.get(platform):
                device =  self._new_upload_devices.get(platform).get(device_id)
                device['linked_account'] = device['linked_account'] | linked_account
                # _LOGGER.debug("[bindManager]  1.linked_account:%s", device['linked_account'])
            else:
                linked_account =linked_account | set(['@' + platform for pplatform in platform])
                device = {
                    'device_id': device_id,
                    'linked_account': linked_account,
                }
                # _LOGGER.debug("[bindManager]  1.linked_account:%s", device['linked_account'])
                self._new_upload_devices.get(platform)[device_id] = device

    async def async_save(self, platform, p_user_id= '*'):
        devices = {}         
        for device_id in self.get_unbind_entity_ids(platform, p_user_id):
            if device_id in devices:
                device =  devices.get(device_id)
                device['linked_account'] = device['linked_account'] | linked_account
                # _LOGGER.debug("1.linked_account:%s", device['linked_account'])
            else:
                linked_account =set([p_user_id +'@'+platform])
                device = {
                    'device_id': device_id,
                    'linked_account': linked_account,
                }
                # _LOGGER.debug("1.linked_account:%s", device['linked_account'])
                devices[device_id] = device
        _LOGGER.debug("[bindManager]  all_unbind_devices:\n%s", devices)

        upload_devices  = [
            {
            'device_id': device_id,
            'linked_account': list((self._privious_upload_devices.get(device_id,{}).get('linked_account',set()) | self._new_upload_devices.get(platform).get(device_id,{}).get('linked_account',set())) - devices.get(device_id,{}).get('linked_account',set()))
            } for device_id in set(list(self._privious_upload_devices.keys())+list(self._new_upload_devices.get(platform).keys()))
        ]
        _LOGGER.debug("[bindManager] upload_devices:\n%s", upload_devices)
        data = {
            'upload_devices':upload_devices,
            'discovery':self.discovery
        }
        await self._store.async_save(data)
        self._privious_upload_devices = {
                    device['device_id']: {'device_id': device['device_id'], 'linked_account': set(device['linked_account'])} for device in upload_devices
            }

    async def async_save_changed_devices(self, new_devices, platform, p_user_id = '*', force_save = False):
        self.update_lists(new_devices, platform)
        uid = p_user_id+'@'+platform
        if self.check_discovery(uid) and not force_save:
            # _LOGGER.debug("[bindManager] 用户(%s)已执行discovery", uid)
            bind_entity_ids = []
            unbind_entity_ids = []
        else:
            # _LOGGER.debug("用户(%s)启动首次执行discovery", uid)
            self.add_discovery(uid)
            bind_entity_ids = self.get_bind_entity_ids(platform = platform,p_user_id =p_user_id, repeat_upload = False)
            unbind_entity_ids = self.get_unbind_entity_ids(platform = platform,p_user_id=p_user_id)
            await self.async_save(platform, p_user_id=p_user_id)
        # _LOGGER.debug("[bindManager] p_user_id:%s',p_user_id)
        # _LOGGER.debug("[bindManager] get_bind_entity_ids:%s", bind_entity_ids)
        # _LOGGER.debug("[bindManager] get_unbind_entity_ids:%s", unbind_entity_ids)
        return bind_entity_ids,unbind_entity_ids

    def check_discovery(self, uid):
        if uid in self._discovery:
            return True
        else:
            return False
    def add_discovery(self, uid):
        self._discovery = self._discovery | set([uid])

    @property
    def discovery(self):
        return list(self._discovery)

    def get_uids(self, platform, device_id):
        # _LOGGER.debug("[bindManager] %s", self._discovery)
        # _LOGGER.debug("[bindManager] %s", self._privious_upload_devices)
        p_user_ids = []
        for uid in self._discovery:
            p_user_id = uid.split('@')[0]
            p = uid.split('@')[1]
            if p == platform and (set([uid, '*@' + platform]) & self._privious_upload_devices.get(device_id,{}).get('linked_account',set())):
                p_user_ids.append(p_user_id)
        return p_user_ids

    async def async_bind_device(self):
        for uuid in self._hass.data[INTEGRATION][DATA_HAVCS_BIND_MANAGER].discovery:
            p_user_id = uuid.split('@')[0]
            platform = uuid.split('@')[1]
            if platform in self._hass.data[INTEGRATION][DATA_HAVCS_HANDLER] and getattr(self._hass.data[INTEGRATION][DATA_HAVCS_HANDLER].get(platform), 'should_report_when_starup', False) and hasattr(self._hass.data[INTEGRATION][DATA_HAVCS_HANDLER].get(platform), 'bind_device'):
                err_result, devices, entity_ids = self._hass.data[INTEGRATION][DATA_HAVCS_HANDLER][platform].process_discovery_command()
                if err_result:
                    return
                bind_entity_ids, unbind_entity_ids = await self._hass.data[INTEGRATION][DATA_HAVCS_BIND_MANAGER].async_save_changed_devices(entity_ids,platform, p_user_id,True)
                payload = await self._hass.data[INTEGRATION][DATA_HAVCS_HANDLER][platform].bind_device(p_user_id, entity_ids , unbind_entity_ids, devices)
                _LOGGER.debug("[skill] bind device to %s:\nbind_entity_ids = %s, unbind_entity_ids = %s", platform, bind_entity_ids, unbind_entity_ids)

                if payload:
                    url = HAVCS_SERVICE_URL + '/skill/smarthome.php?v=update&AppKey='+self._sync_manager.get('app_key')
                    data = havcs_util.AESCipher(self._sync_manager.get('decrypt_key')).encrypt(json.dumps(payload, ensure_ascii = False).encode('utf8'))
                    try:
                        session = async_get_clientsession(self._hass, verify_ssl=False)
                        with async_timeout.timeout(5, loop=self._hass.loop):
                            response = await session.post(url, data=data)
                            _LOGGER.debug("[skill] get bind device result from %s: %s", platform, await response.text())
                    except(asyncio.TimeoutError, aiohttp.ClientError):
                        _LOGGER.error("[skill] fail to access %s, bind device fail: timeout", url)
                    except:
                        _LOGGER.error("[skill] fail to access %s, bind device fail: %s", url, traceback.format_exc())
    
    def sync_device(self):
        remove_listener = self._sync_manager.get('remove_listener')
        if remove_listener:
            remove_listener()
    
        @callback
        def report_device(event):
            # _LOGGER.debug("[skill] %s changed, try to report", event.data[ATTR_ENTITY_ID])
            self._hass.add_job(async_report_device(event))

        async def async_report_device(event):
            """report device state when changed. """
            entity = self._hass.states.get(event.data[ATTR_ENTITY_ID])
            if entity is None:
                return
            for platform, handler in self._hass.data[INTEGRATION][DATA_HAVCS_HANDLER].items():
                if hasattr(handler, 'report_device'):
                    device_ids = handler.vcdm.get_entity_related_device_ids(self._hass, entity.entity_id)
                    for device_id in device_ids:
                        payload = handler.report_device(device_id)
                        _LOGGER.debug("[skill] report device to %s: platform = %s, device_id = %s (entity_id = %s), data = %s", platform, device_id, event.data[ATTR_ENTITY_ID], platform, payload)
                        if payload:
                            url = HAVCS_SERVICE_URL + '/skill/'+platform+'.php?v=report&AppKey=' + self._sync_manager.get('app_key')
                            data = havcs_util.AESCipher(self._sync_manager.get('decrypt_key')).encrypt(json.dumps(payload, ensure_ascii = False).encode('utf8'))
                            try:
                                session = async_get_clientsession(self._hass, verify_ssl=False)
                                with async_timeout.timeout(5, loop=self._hass.loop):
                                    response = await session.post(url, data=data)
                                    _LOGGER.debug("[skill] get report device result from %s: %s", platform, await response.text())
                            except(asyncio.TimeoutError, aiohttp.ClientError):
                                _LOGGER.error("[skill] fail to access %s, report device fail: timeout", url)
                            except:
                                _LOGGER.error("[skill] fail to access %s, report device fail: %s", url, traceback.format_exc())
        
        self._sync_manager['remove_listener'] = self._hass.bus.async_listen(EVENT_STATE_CHANGED, report_device)

    def clear(self):
        remove_listener = self._sync_manager.get('remove_listener')
        if remove_listener:
            remove_listener()