import logging

from gql import gql, Client
from gql.transport.aiohttp import AIOHTTPTransport
from aiohttp import CookieJar
from bos_toolbox.util import get_resource_path
from bos_toolbox.platform.descriptor import TargetTriple

LOG = logging.getLogger(__name__)


def load_operation(name):
    handle = open(get_resource_path(f'data/graphql/{name}.graphql'), 'r')
    contents = handle.read()
    return gql(contents)


AUTHENTICATE_MUTATION = load_operation('authenticate')
LOAD_CONFIG_QUERY_V1 = load_operation('loadConfigV1')
LOAD_CONFIG_QUERY = load_operation('loadConfig')
UPDATE_CONFIG_MUTATION = load_operation('updateConfig')
UPDATE_CONFIG_PERFORMANCE_MUTATION = load_operation('updateConfigPerformance')
UPDATE_CONFIG_AUTOTUNING_MUTATION_V1 = load_operation('updateConfigAutotuningV1')
UPDATE_CONFIG_AUTOTUNING_MUTATION = load_operation('updateConfigAutotuning')
UPDATE_AUTO_UPGRADE = load_operation('updateAutoUpgrade')
SET_PASSWORD_MUTATION = load_operation('setPassword')


def replace_none(data):
    keys_to_delete = []
    for k, v in data.items() if isinstance(data, dict) else enumerate(data):
        if v is None:
            keys_to_delete.append(k)
        elif isinstance(v, (dict, list)):
            replace_none(v)
    for k in keys_to_delete:
        del data[k]


def rename_keys(tuples, data):
    for (from_key, to_key) in tuples:
        if from_key in data:
            current = data
            key_arr = to_key.split('.')
            for k in key_arr[0:-1]:
                current = current.setdefault(k, {})
            current[key_arr.pop()] = data.pop(from_key)


class BosGQLApi:
    def __init__(self):
        self.client = None
        self.v2 = None

    def connect_and_authenticate(self, host, username='root', password=''):
        jar = CookieJar(unsafe=True)
        transport = AIOHTTPTransport(
            url=f'http://{host}/graphql', client_session_args={'cookie_jar': jar}
        )
        self.client = Client(transport=transport, fetch_schema_from_transport=True)
        self.client.execute(
            AUTHENTICATE_MUTATION,
            variable_values={'username': username, 'password': password},
        )

        # detect if it's a new version based on the presence of `hashrateTarget`
        self.v2 = (
            'hashrateTarget'
            in self.client.schema.get_type('Autotuning').to_kwargs()['fields']
        )

        return self

    def get_config(self):
        """Read miner config"""

        if self.v2:
            result = self.client.execute(LOAD_CONFIG_QUERY)
        else:
            result = self.client.execute(LOAD_CONFIG_QUERY_V1)

        if result.get('errors'):
            raise ConfigFetchFailed(result.get('errors'))

        replace_none(result)
        output = result['bosminer']['config']
        output['auto_upgrade'] = result['bos'].get('auto_upgrade')
        output['format'] = result['bosminer']['format']
        output['format']['version'] = result['bos']['info']['version']
        output.setdefault('hash_chain_global', {})

        triple = TargetTriple._get_target_triple(output['format']['model'])

        return output, triple

    def set_config(self, data, apply=False):
        """Write miner config"""
        temp_and_fans = self._pluck_temp_and_fans_data(data)
        autotuning = self._pluck_autotuning_data(data)
        performance = self._pluck_performance_data(data)
        group_id, pools = self._pluck_pools(data)

        if data.get('autotuning', {}).get('enabled', False):
            result = self.client.execute(
                UPDATE_CONFIG_AUTOTUNING_MUTATION
                if self.v2
                else UPDATE_CONFIG_AUTOTUNING_MUTATION_V1,
                variable_values={'autotuningInput': autotuning, 'apply': False},
            )
            autotuning_errors = result['bosminer']['config']['updateAutotuning']
            if self.v2:
                self._show_error(
                    autotuning_errors,
                    ('mode', 'powerTarget', 'hashrateTarget'),
                    'autotuning',
                )
                self._show_error(
                    autotuning_errors.get('performanceScaling'),
                    (
                        'powerStep',
                        'minPowerTarget',
                        'hashrateStep',
                        'minHashrateTarget',
                        'shutdownDuration',
                    ),
                    'power_scaling',
                )
            else:
                self._show_error(autotuning_errors, ('psuPowerLimit',), 'autotuning')
                self._show_error(
                    autotuning_errors.get('powerScaling'),
                    ('minPsuPowerLimit', 'shutdownDuration', 'powerStep'),
                    'power_scaling',
                )
        else:
            result = self.client.execute(
                UPDATE_CONFIG_PERFORMANCE_MUTATION,
                variable_values={'performanceInput': performance, 'apply': False},
            )
            self._show_error(
                result['bosminer']['config']['updatePerformance'],
                ('globalFrequency', 'globalVoltage'),
                'performance',
            )
            for hashchain_error in result['bosminer']['config'][
                'updatePerformance'
            ].get('hashChains', []):
                name = hashchain_error['name']
                self._show_error(
                    hashchain_error,
                    ('frequency', 'voltage'),
                    f'performance: chain {name}',
                )

        result = self.client.execute(
            UPDATE_CONFIG_MUTATION,
            variable_values={
                'tempAndFansInput': temp_and_fans,
                'group_id': group_id,
                'pools': pools,
                'apply': apply,
            },
        )
        self._show_error(
            result['bosminer']['config']['updateGroups'], ('message',), 'groups'
        )
        self._show_error(
            result['bosminer']['config']['updateTempAndFans'],
            ('targetTemp', 'hotTemp', 'dangerousTemp', 'speed', 'minFans'),
            'temp_and_fans',
        )

        auto_upgrade = data['auto_upgrade']
        if auto_upgrade is not None:
            result = self.client.execute(
                UPDATE_AUTO_UPGRADE, variable_values={'autoUpgrade': auto_upgrade}
            )
            self._show_error(result['bos']['autoUpgrade'], ('message',), 'auto_upgrade')

    @staticmethod
    def _show_error(errors, keys, tag):
        errors = errors or {}
        for key in keys:
            error = errors.get(key)
            if error:
                LOG.warning(f'{tag}: {error}')

    def _pluck_temp_and_fans_data(self, data):
        temp_ctl = data.get('temp_control', {})
        if temp_ctl.get('mode', False):
            temp_ctl['mode'] = temp_ctl['mode'].upper()

        rename_keys(
            [
                ('dangerous_temp', 'dangerousTemp'),
                ('hot_temp', 'hotTemp'),
                ('target_temp', 'targetTemp'),
            ],
            temp_ctl,
        )

        fan_ctl = data.get('fan_control', {})
        rename_keys([('min_fans', 'minFans')], fan_ctl)

        temp_and_fans_ctl = temp_ctl
        temp_and_fans_ctl.update(fan_ctl)

        return temp_and_fans_ctl

    def _pluck_performance_data(self, data):
        performance = data['hash_chain_global']

        rename_keys(
            [
                ('asic_boost', 'asicBoost'),
                ('frequency', 'globalFrequency'),
                ('voltage', 'globalVoltage'),
            ],
            performance,
        )

        performance['hashChains'] = list(data.get('hash_chain', {}).values())
        return performance

    def _pluck_autotuning_data(self, data):
        autotuning = data.get('autotuning', {}).copy()
        powerScaling = data.get('power_scaling', {}).copy()

        rename_keys(
            [
                ('psu_power_limit', 'powerTarget' if self.v2 else 'psuPowerLimit'),
                ('hashrate_target', 'hashrateTarget'),
            ],
            autotuning,
        )

        rename_keys(
            [
                ('power_step', 'powerStep'),
                (
                    'min_psu_power_limit',
                    'minPowerTarget' if self.v2 else 'minPsuPowerLimit',
                ),
                ('hashrate_step', 'hashrateStep'),
                ('min_hashrate_target', 'minHashrateTarget'),
                ('shutdown_enabled', 'shutdownEnabled'),
                ('shutdown_duration', 'shutdownDuration'),
            ],
            powerScaling,
        )

        autotuning['performanceScaling' if self.v2 else 'powerScaling'] = powerScaling
        autotuning.pop('enabled')

        hash_chains = []
        for hash_chain in data.get('hash_chain', {}).values():
            hash_chains.append(
                {
                    key: value
                    for key, value in hash_chain.items()
                    if key in ('name', 'enabled')
                }
            )

        autotuning['hashChains'] = hash_chains
        return autotuning

    def _pluck_pools(self, data):
        group_id = data['group'][0]['id']
        pool = data['group'][0]['pool']
        return group_id, pool

    def apply_config(self):
        pass

    def uci_init_section(self, x0, x1, x2):
        pass

    def uci_set_bool(self, x0, x1, x2, x3):
        pass

    def uci_commit(self, x0):
        pass

    def set_password(self, password):
        result = self.client.execute(
            SET_PASSWORD_MUTATION, variable_values={'newPassword': password}
        )
        self._show_error(result['bos']['setPassword'], ('message',), 'password')


class GraphQLError(RuntimeError):
    def __init__(self, errors):
        error = [e['message'] for e in errors].join('. ')
        super().__init__(f'{error}')


class ConfigFetchFailed(GraphQLError):
    pass
