# -*- coding: utf-8 -*-
import attr
import logging
import urllib.request, urllib.parse, urllib.error
import urllib.parse

from django.utils.translation import ugettext_lazy as _

from idm.core.plugins.base import BasePlugin
from idm.core.plugins.plugins_mixin import RequestMixin, _DEFAULT_TIMEOUT
from idm.utils import json, coroutine
from idm.utils.check_switch import check_switch
from idm.core.exceptions import PushDisabled

log = logging.getLogger(__name__)


HANDLE_CHOICES = [(key, key) for key in ('info', 'get-all-roles', 'get-roles')]
LIBRARY_CHOICES = (
    ('default', _('Прописанная в системе')),
    ('requests', 'requests'),
    ('curl', 'curl'),
)


@attr.s
class PluginField:
    key: str = attr.ib()
    src_key: str = attr.ib(default=None)
    optional: bool = attr.ib(default=False)
    default: str = attr.ib(default=None)
    json: bool = attr.ib(default=False)


class Plugin(BasePlugin, RequestMixin):
    """Это плагин для систем, которые поддерживают стандартный API.

    Этот плагин в конструкторе принимает базовый URL по которому доступно API системы.
    Ручки API описанны на вики: http://wiki.yandex-team.ru/Upravljator/API
    """

    ROLE_PUSH_SCHEMA = [
        PluginField(key='role', src_key='role_data', json=True),
        PluginField(key='fields', src_key='fields_data', json=True),
        PluginField(key='path'),
        PluginField(key='login', src_key='username', optional=True),
        PluginField(key='group', src_key='group_id', optional=True),
        PluginField(key='uid', optional=True),
        PluginField(key='unique_id', optional=True, default=''),
        PluginField(key='subject_type', optional=True),
    ]

    def get_next_url(self, next_url):
        if not next_url:
            return
        if '://' in next_url:
            if not next_url.startswith(self.host):
                # не даём выйти за пределы базового урла
                return
        else:
            next_url = self.host + next_url
        return next_url

    def get_roles_tree(self, tree):
        """Формирует дерево ролей, рекурсивно выкачивая роли по ссылкам 'next-url'"""
        if isinstance(tree, dict) and 'roles' in tree:
            next_url = tree['roles']['values'].get('next-url')
            if next_url:
                del tree['roles']['values']['next-url']
                next_url = self.get_next_url(next_url)
                next_roles = self._send_data(next_url, method='GET', timeout=60)
                self.get_roles_tree(next_roles)
                tree['roles']['values'].update(next_roles['roles']['values'])

            for k, v in tree['roles']['values'].items():
                self.get_roles_tree(v)

    def get_info(self, **kwargs):
        """Роли, запрошенные у системы"""
        # внедрим сюда итерацию по ролям
        # считаем первые данные, пройдемся по ним и для каждой встреченной метки 'next-url'
        # будем рекурсивно выкачивать роли
        # 'next-url' ищем всегда в 'values'

        try:
            roles = self._fetch_data('info', timeout=self.system.endpoint_long_timeout)
            self.get_roles_tree(roles)

        except Exception:
            log.warning('during get_info from %s with params %s', self.system.slug, kwargs, exc_info=1)

            raise

        return roles

    def get_all_roles(self, **kwargs):
        # делаем таймаут побольше, чтобы система успела ответить
        result = self._fetch_data('get-all-roles', timeout=self.system.endpoint_long_timeout)
        return result

    @coroutine
    def get_roles(self, **kwargs):
        return self.get_objects('get-roles', 'roles', **kwargs)

    @coroutine
    def get_memberships(self, **kwargs):
        return self.get_objects('get-memberships', 'memberships', **kwargs)

    def get_objects(self, handle_name, field_name, **kwargs):
        url = self.base_url % handle_name
        data = self._send_data(url, 'GET', data=kwargs.get('data', None), timeout=self.system.endpoint_long_timeout)
        _ = yield  # притормозим генератор
        while True:
            objects = data.get(field_name)
            for element in objects:
                yield element
            url = data.get('next-url')
            if url:
                url = self.get_next_url(url)
            if url is None:
                return
            data = self._send_data(url, 'GET', timeout=self.system.endpoint_long_timeout)

    @staticmethod
    def make_headers(request_id=None, **_):
        headers = {}
        if request_id is not None:
            headers['X-IDM-Request-Id'] = str(request_id)

        return headers

    @check_switch('disable_pushes_all', 'disable_pushes_add_roles', raise_=PushDisabled())
    def add_role(self, **kwargs):
        push_data = self.make_push_data(self.ROLE_PUSH_SCHEMA, kwargs)
        self.log_push(push_data)
        return self._post_data('add-role', data=push_data, headers=self.make_headers(**kwargs))

    @check_switch('disable_pushes_all', 'disable_pushes_remove_roles', raise_=PushDisabled())
    def remove_role(self, **kwargs):
        push_data = self.make_push_data(self.ROLE_PUSH_SCHEMA, kwargs, for_remove=True)
        self.log_push(push_data, for_remove=True)
        return self._post_data('remove-role', data=push_data, headers=self.make_headers(**kwargs))

    def make_push_data(self, schema, kwargs, for_remove=False):
        """
        Prepare body for POST request to system
        """
        data = {}
        if for_remove and kwargs.get('is_fired'):
            fired_field = 'fired' if kwargs.get('username') else 'deleted'
            data[fired_field] = 1
        for field in schema:
            if field.key == 'uid' and not self.system.push_uid:
                continue  # TODO: remove eventually
            key = field.src_key or field.key
            value = kwargs.get(key, field.default)
            if field.json:
                try:
                    value = json.dumps(value)
                except ValueError:
                    log.exception(f'Cannot encode to JSON ({field.key}): {value}')
                    raise
            if not field.optional or value != field.default:
                data[field.key] = value
        return data

    def log_push(self, role_data, for_remove=False, ident=None):
        if ident is None:
            if role_data.get('login') is not None:
                ident = 'user %s' % role_data['login']
            else:
                ident = 'group %s' % role_data['group']
        log.info('Push for %s: %s role for %s: role=%s, fields=%s',
                 ('REMOVE' if for_remove else 'ADD'), self.system.slug, ident, role_data['role'],
                 role_data.get('fields') or role_data['data'])

    @check_switch('disable_pushes_all', 'disable_pushes_add_memberships', raise_=PushDisabled())
    def add_group_membership(self, data, **kwargs):
        try:
            serialized_data = json.dumps(data)
        except ValueError:
            log.exception('Cannot encode to JSON (group_membership): %s', data)
            raise
        return self._post_data('add-batch-memberships', {'data': serialized_data}, headers=self.make_headers(**kwargs))

    @check_switch('disable_pushes_all', 'disable_pushes_remove_memberships', raise_=PushDisabled())
    def remove_group_membership(self, data, **kwargs):
        try:
            serialized_data = json.dumps(data)
        except ValueError:
            log.exception('Cannot encode to JSON (group_membership): %s', data)
            raise
        return self._post_data('remove-batch-memberships', {'data': serialized_data},
                               headers=self.make_headers(**kwargs))

    def _fetch_data(self, method, timeout=_DEFAULT_TIMEOUT, **kwargs):
        url = self.base_url % method
        if kwargs:
            if '?' in self.base_url:
                url += '&' + urllib.parse.urlencode(kwargs)
            else:
                url += '?' + urllib.parse.urlencode(kwargs)

        return self._send_data(url, method='GET', timeout=timeout)

    def _post_data(self, method, data, timeout=None, headers=None):
        timeout = timeout or self.system.endpoint_timeout or _DEFAULT_TIMEOUT
        url = self.base_url % method
        return self._send_data(url, data=data, method='POST', timeout=timeout, headers=headers)
