# -*- coding: utf-8 -*-
import logging
import yenv

from django.conf import settings
from django.utils.cache import caches

from events.common_app.utils import (
    get_lang_with_fallback,
    get_user_ip_address,
    requests_session,
)
from events.yauth_contrib.auth import TvmAuth

logger = logging.getLogger(__name__)

if yenv.type == 'production':
    DIRECTORY_HOST = 'https://api-internal.directory.ws.yandex.net'
else:
    DIRECTORY_HOST = 'https://api-integration-qa.directory.ws.yandex.net'


class DirectoryError(Exception):
    pass


class DirectoryUserNotFoundError(DirectoryError):
    pass


class DirectoryGroupNotFoundError(DirectoryError):
    pass


class DirectoryDepartmentNotFoundError(DirectoryError):
    pass


class DirectoryOrganizationsListEmptyError(DirectoryError):
    pass


class DirectoryClient:
    per_page = 1000

    def __init__(self):
        self.auth = TvmAuth(settings.DIRECTORY_TVM2_CLIENT)
        self.user_ip = get_user_ip_address()

    def _get_one(self, url, params=None, headers=None):
        response = requests_session.get(url, params=params, headers=headers, auth=self.auth)
        if response.status_code != 200:
            logger.warning('DirectoryClient: status(%s), body(%s)',
                           response.status_code, response.text)
            return None
        return response.json()

    def _get_many(self, url, params=None, headers=None):
        while url:
            response = requests_session.get(url, params=params, headers=headers, auth=self.auth)
            if response.status_code != 200:
                logger.warning('DirectoryClient: status(%s), body(%s)',
                               response.status_code, response.text)
                break
            data = response.json()
            result = data['result'] or []
            for it in result:
                yield it
            links = data['links'] or {}
            url = links.get('next')
            params = None

    def get_organizations(self, uid, cloud_uid=None, fields=None):
        fields = fields or 'id,name'
        url = f'{DIRECTORY_HOST}/v6/organizations/'
        params = {
            'fields': fields,
        }
        headers = {
            'X-User-IP': self.user_ip,
        }
        if uid:
            headers['X-UID'] = uid
        if cloud_uid:
            headers['X-Cloud-UID'] = cloud_uid
        return self._get_many(url, params, headers)

    def get_organization(self, dir_id, fields=None):
        fields = fields or 'id,name'
        url = f'{DIRECTORY_HOST}/v6/organizations/{dir_id}/'
        params = {
            'fields': fields,
        }
        headers = {
            'X-Org-ID': dir_id,
            'X-User-IP': self.user_ip,
        }
        return self._get_one(url, params, headers)

    def get_users(self, dir_id, fields=None):
        fields = fields or 'id,cloud_uid,name,nickname'
        url = f'{DIRECTORY_HOST}/v6/users/'
        params = {
            'fields': fields,
            'per_page': self.per_page,
        }
        headers = {
            'X-Org-ID': dir_id,
            'X-User-IP': self.user_ip,
        }
        yield from self._get_many(url, params, headers)

    def get_user(self, dir_id, uid, cloud_uid=None, fields=None):
        if not uid and not cloud_uid:
            return
        fields = fields or 'id,cloud_uid,name,nickname'
        params = {
            'fields': fields,
        }
        headers = {
            'X-Org-ID': dir_id,
            'X-User-IP': self.user_ip,
        }
        if uid:
            url = f'{DIRECTORY_HOST}/v6/users/{uid}/'
            headers['X-UID'] = uid
        elif cloud_uid:
            url = f'{DIRECTORY_HOST}/v6/users/cloud/{cloud_uid}/'
            headers['X-Cloud-UID'] = cloud_uid
        return self._get_one(url, params, headers)

    def get_groups(self, dir_id, fields=None):
        fields = fields or 'id,name'
        url = f'{DIRECTORY_HOST}/v6/groups/'
        params = {
            'fields': fields,
            'per_page': self.per_page,
        }
        headers = {
            'X-Org-ID': dir_id,
            'X-User-IP': self.user_ip,
        }
        yield from self._get_many(url, params, headers)

    def get_group(self, dir_id, group_id, fields=None):
        fields = fields or 'id,name'
        url = f'{DIRECTORY_HOST}/v6/groups/{group_id}/'
        params = {
            'fields': fields,
        }
        headers = {
            'X-Org-ID': dir_id,
            'X-User-IP': self.user_ip,
        }
        return self._get_one(url, params, headers)

    def get_departments(self, dir_id, fields=None):
        fields = fields or 'id,name'
        url = f'{DIRECTORY_HOST}/v6/departments/'
        params = {
            'fields': fields,
            'per_page': self.per_page,
        }
        headers = {
            'X-Org-ID': dir_id,
            'X-User-IP': self.user_ip,
        }
        yield from self._get_many(url, params, headers)

    def get_department(self, dir_id, department_id, fields=None):
        fields = fields or 'id,name'
        url = f'{DIRECTORY_HOST}/v6/departments/{department_id}/'
        params = {
            'fields': fields,
        }
        headers = {
            'X-Org-ID': dir_id,
            'X-User-IP': self.user_ip,
        }
        return self._get_one(url, params, headers)

    def who_is(self, domain=None, email=None):
        if not domain and not email:
            return {}
        url = f'{DIRECTORY_HOST}/v6/who-is/'
        params = {}
        if domain:
            params['domain'] = domain
        if email:
            params['email'] = email
        headers = {
            'X-User-IP': self.user_ip,
        }
        return self._get_one(url, params, headers)

    def get_service_user(self, dir_id, service_slug, fields=None):
        fields = fields or 'id'
        params = {
            'service_slug': service_slug,
            'fields': fields,
        }
        headers = {
            'X-Org-ID': dir_id,
            'X-User-IP': self.user_ip,
        }
        url = f'{DIRECTORY_HOST}/v6/users/'
        result = sorted(
            self._get_many(url, params, headers),
            key=lambda x: x['id'],
            reverse=True,
        )
        if not result:
            return None
        return result[0]['id']


class CachedDirectoryClient(DirectoryClient):
    not_found = object()

    def __init__(self):
        super().__init__()
        self.cache = caches['dir']

    def get_organizations(self, uid, cloud_uid=None, fields=None):
        key = f'o:{uid or ""}:{cloud_uid or ""}:{fields or ""}'
        result = self.cache.get(key, self.not_found)
        if result is self.not_found:
            result = list(super().get_organizations(uid, cloud_uid=cloud_uid, fields=fields))
            self.cache.set(key, result)
        return result

    def get_organization(self, dir_id, fields=None):
        key = f'o:{dir_id}:{fields or ""}'
        result = self.cache.get(key, self.not_found)
        if result is self.not_found:
            result = super().get_organization(dir_id, fields=fields)
            self.cache.set(key, result)
        return result

    def get_users(self, dir_id, fields=None):
        key = f'u:{dir_id}:{fields or ""}'
        result = self.cache.get(key, self.not_found)
        if result is self.not_found:
            result = list(super().get_users(dir_id, fields=fields))
            self.cache.set(key, result)
        return result

    def get_user(self, dir_id, uid, cloud_uid=None, fields=None):
        key = f'u:{uid or ""}:{cloud_uid or ""}:{fields or ""}'
        result = self.cache.get(key, self.not_found)
        if result is self.not_found:
            result = super().get_user(dir_id, uid, cloud_uid=cloud_uid, fields=fields)
            self.cache.set(key, result)
        return result

    def get_groups(self, dir_id, fields=None):
        key = f'g:{dir_id}:{fields or ""}'
        result = self.cache.get(key, self.not_found)
        if result is self.not_found:
            result = list(super().get_groups(dir_id, fields=fields))
            self.cache.set(key, result)
        return result

    def get_group(self, dir_id, group_id, fields=None):
        key = f'g:{dir_id}:{group_id}:{fields or ""}'
        result = self.cache.get(key, self.not_found)
        if result is self.not_found:
            result = super().get_group(dir_id, group_id, fields=fields)
            self.cache.set(key, result)
        return result

    def get_departments(self, dir_id, fields=None):
        key = f'd:{dir_id}:{fields or ""}'
        result = self.cache.get(key, self.not_found)
        if result is self.not_found:
            result = list(super().get_departments(dir_id, fields=fields))
            self.cache.set(key, result)
        return result

    def get_department(self, dir_id, department_id, fields=None):
        key = f'd:{dir_id}:{department_id}:{fields or ""}'
        result = self.cache.get(key, self.not_found)
        if result is self.not_found:
            result = super().get_department(dir_id, department_id, fields=fields)
            self.cache.set(key, result)
        return result

    def who_is(self, domain=None, email=None):
        key = f'w:{domain or ""}:{email or ""}'
        result = self.cache.get(key, self.not_found)
        if result is self.not_found:
            result = super().who_is(domain=domain, email=email)
            self.cache.set(key, result)
        return result

    def get_service_user(self, dir_id, service_slug, fields=None):
        key = f's:{dir_id}:{service_slug}:{fields or ""}'
        result = self.cache.get(key, self.not_found)
        if result is self.not_found:
            result = super().get_service_user(dir_id, service_slug, fields=fields)
            self.cache.set(key, result)
        return result


def get_translated_dir_data(dir_data, field_name, language):
    language, fallback_lang = get_lang_with_fallback(language)
    field_value = dir_data.get(field_name)
    if isinstance(field_value, dict):
        if language in field_value:
            return field_value[language]
        return field_value.get(fallback_lang, '')
    return field_value


def build_real_user_name(dir_data, language):
    """
    Конструирует строку с ФИО пользователя из данных об имени пользователя, полученных из Директории, вида
    {
      "name": {
        "last": {
          "ru": "Админ"
        },
        "first": {
          "ru": "Главный"
        }
    }
    {
      "name": {
        "last": "Doe",
        "first": "John"
      }
    }

    @param language lower-case language code, e.g. 'ru'
    @param dir_data данные из АПИ Директории
    @rtype: unicode
    """
    fio = ' '.join(
        fio_part
        for fio_part in (
            get_translated_dir_data(dir_data, field_name, language)
            for field_name in ('first', 'middle', 'last')
        )
        if fio_part
    )
    if fio:
        return fio


def get_text_from_field(field, language):
    if isinstance(field, str):
        return field
    elif isinstance(field, dict):
        for lang in (language, 'ru', 'en'):
            text = field.get(lang)
            if text:
                return text


class DirectorySuggest:
    not_found = object()

    def __init__(self, dir_id, language=None):
        self.dir_id = dir_id
        self.language = language or 'ru'
        self.layers = {
            'people': self.get_people,
            'departments': self.get_departments,
            'groups': self.get_groups,
        }
        self.client = DirectoryClient()

    def _get_person_data(self, userinfo):
        name = userinfo.get('name') or {}
        return {
            'id': str(userinfo['id']),
            'cloud_uid': userinfo.get('cloud_uid'),
            'title': build_real_user_name(name, self.language),
            'layer': 'people',
            'fields': [
                {
                    'type': 'login',
                    'value': userinfo.get('nickname'),
                },
            ],
            'is_dismissed': False,
            'url': None,
            'click_urls': [],
            'avatar_url': None,
        }

    def _get_people(self):
        fields = 'name,nickname,cloud_uid'
        return [
            self._get_person_data(userinfo)
            for userinfo in self.client.get_users(self.dir_id, fields=fields)
        ]

    def _is_valid(self, item, text, ids):
        if text:
            lowered = text.lower()
            if lowered in (item.get('title') or '').lower():
                return True
            for field in item.get('fields') or []:
                if lowered in (field.get('value') or '').lower():
                    return True
        if ids:
            if item['id'] in ids:
                return True
        return False

    def get_people(self, text, ids):
        if not text and not ids:
            return []
        cache = caches['userinfo']
        cache_key = f'{self.dir_id}:{self.language}:suggest'
        data = cache.get(cache_key, self.not_found)
        if data is self.not_found:
            data = self._get_people()
            cache.set(cache_key, data)
        result = [
            it
            for it in data or []
            if self._is_valid(it, text, ids)
        ]
        return result

    def _get_department_data(self, depinfo):
        return {
            'id': str(depinfo['id']),
            'title': depinfo.get('name'),
            'layer': 'departments',
            'url': None,
            'click_urls': [],
        }

    def _get_departments(self):
        return [
            self._get_department_data(depinfo)
            for depinfo in self.client.get_departments(self.dir_id)
        ]

    def get_departments(self, text, ids):
        if not text and not ids:
            return []
        cache = caches['depinfo']
        cache_key = f'{self.dir_id}:suggest'
        data = cache.get(cache_key, self.not_found)
        if data is self.not_found:
            data = self._get_departments()
            cache.set(cache_key, data)
        result = [
            it
            for it in data or []
            if self._is_valid(it, text, ids)
        ]
        return result

    def _get_group_data(self, groupinfo):
        return {
            'id': str(groupinfo['id']),
            'title': groupinfo.get('name'),
            'layer': 'groups',
            'url': None,
            'click_urls': [],
        }

    def _get_groups(self):
        return [
            self._get_group_data(groupinfo)
            for groupinfo in self.client.get_groups(self.dir_id)
        ]

    def get_groups(self, text, ids):
        if not text and not ids:
            return []
        cache = caches['groupinfo']
        cache_key = f'{self.dir_id}:suggest'
        data = cache.get(cache_key, self.not_found)
        if data is self.not_found:
            data = self._get_groups()
            cache.set(cache_key, data)
        result = [
            it
            for it in data or []
            if self._is_valid(it, text, ids)
        ]
        return result

    def suggest(self, text=None, ids=None, layers=None):
        layers = layers or self.layers.keys()
        result = {}
        for layer in layers:
            method = self.layers.get(layer)
            if method:
                result[layer] = {
                    'result': method(text, ids),
                    'pagination': {},
                }
        return result


class BiSearchClient:
    def __init__(self, uid, cloud_uid, dir_id, language=None):
        self.dir_id = dir_id
        self.language = language or 'ru'
        self.headers = {
            'X-UID': uid,
            'X-Cloud-UID': cloud_uid,
            'X-Org-ID': dir_id,
            'X-User-IP': get_user_ip_address(),
        }
        self.auth = TvmAuth(settings.INTRASEARCH_TVM2_CLIENT)

    def suggest(self, text=None, layers=None):
        params = {
            'version': 2,
            'allow_empty': 1,
            'language': self.language,
            'layers': ','.join(layers),
            'text': text,
            'org_id': self.dir_id,
        }
        response = requests_session.get(
            f'{settings.INTRASEARCH_HOST}/_abovemeta/suggest/',
            params=params,
            headers=self.headers,
            auth=self.auth,
            timeout=settings.DEFAULT_TIMEOUT,
        )
        if response.status_code != 200:
            logger.error('BiSearchClient error: %s, %r', response.status_code, response.text)
            return {
                layer: {'result': [], 'pagination': {}}
                for layer in layers
            }
        return response.json()


class BiSearchSuggest:
    default_layers = ('people', 'groups', 'departments')

    def __init__(self, uid, cloud_uid, dir_id, language=None):
        self.dir_id = dir_id
        self.language = language or 'ru'
        self.bisearch = BiSearchClient(uid, cloud_uid, dir_id, language)

    def get_people(self, ids):
        client = CachedDirectoryClient()
        for item in client.get_users(self.dir_id):
            user_uid = str(item['id'])
            if user_uid in ids:
                yield {
                    'id': user_uid,
                    'title': build_real_user_name(item['name'], self.language)
                }

    def get_departments(self, ids):
        client = CachedDirectoryClient()
        for item in client.get_departments(self.dir_id):
            department_id = str(item['id'])
            if department_id in ids:
                yield {
                    'id': department_id,
                    'title': get_text_from_field(item['name'], self.language),
                }

    def get_groups(self, ids):
        client = CachedDirectoryClient()
        for item in client.get_groups(self.dir_id):
            group_id = str(item['id'])
            if group_id in ids:
                yield {
                    'id': str(item['id']),
                    'title': get_text_from_field(item['name'], self.language),
                }

    def get_objects(self, ids, layer):
        method = getattr(self, f'get_{layer}', None)
        if method:
            for item in method(ids):
                yield item

    def get_objects_by_ids(self, ids, layers):
        return {
            layer: {
                'result': list(self.get_objects(ids, layer)),
                'pagination': {},
            }
            for layer in layers
        }

    def suggest(self, text=None, ids=None, layers=None):
        layers = layers or self.default_layers
        if isinstance(ids, str):
            ids = ids.split(',')
        if ids:
            return self.get_objects_by_ids(ids, layers)
        return self.bisearch.suggest(text=text, layers=layers)
