import enum
import logging
import time
import typing

from collections import Counter

import requests

import yt.wrapper as yt

ACHIEVEMENT_ID = 1292
LEVELS = [-1, 20, 50, 100]

logger = logging.getLogger(__name__)


def get_level(score):
    for index, item in enumerate(LEVELS):
        if score < item:
            return index - 1
    return len(LEVELS) - 1


@yt.yt_dataclass
class Row:
    id: typing.Optional[int]
    created_at: typing.Optional[str]
    request_id: typing.Optional[str]
    score: typing.Optional[str]
    uid: typing.Optional[str]
    updated_at: typing.Optional[str]
    url: typing.Optional[str]


class UserStatus(enum.Enum):
    without_achieve = 1
    inactive = 2
    wrong_level = 3
    ok = 4


class IntrasearchAchieveryManager:
    def __init__(
        self,
        token,
        table_with_marks,
        yt_token,
        api_url='https://staff.yandex-team.ru/api/achievery/',
        staff_api_url='https://staff-api.yandex-team.ru/v3/persons',
        proxy='hahn',
        timeout=10,
        is_dry_run=False,
    ):
        self._token = token
        self._yt_token = yt_token
        self._table = table_with_marks
        self._achievery_base_url = api_url
        self._achievement_id = ACHIEVEMENT_ID
        self._headers = {
            'authorization': f'OAuth {self._token}',
        }
        self._client = yt.YtClient(token=self._yt_token, proxy=proxy)
        self._staff_api_url = staff_api_url
        self._timeout = timeout
        self._is_dry_run = is_dry_run
        self._max_retries = 3

    def _get_users(self):
        users_table = self._client.read_table_structured(self._table, Row)
        processed_uids = [tmp_row.uid for tmp_row in users_table]
        uid_to_login = self._resolve_uids(processed_uids)
        count_processed_uids = Counter(processed_uids)
        users = {}
        for uid in count_processed_uids:
            uid_entries = count_processed_uids[uid]
            if uid in uid_to_login:
                users[uid] = {
                    'login': uid_to_login[uid],
                    'rated_score': uid_entries,
                }
            else:
                logger.warning('Ignoring %s: can not resolve login', uid)

        return users

    def _call_with_retry(self, session_func, url, **kwargs):
        retries = self._max_retries
        while True:
            try:
                response = session_func(url, **kwargs)
            except Exception:
                retries -= 1
                if retries < 0:
                    logger.exception('Error while request %s', url)
                    raise
                time.sleep(2 ** (self._max_retries - retries))
            else:
                return response

    def _resolve_uids(self, uids):
        uid_to_login = {}  # final answer of the method
        batch_size = 50
        batches_num = (len(uids) + batch_size - 1) // batch_size

        for uid_batch_num in range(batches_num):
            batch_start_index = uid_batch_num * batch_size
            batch_end_index = min(batch_start_index + batch_size, len(uids))
            str_uids = list(map(str, uids[batch_start_index:batch_end_index]))  # convert uids to str
            uids_batch = ','.join(str_uids)  # comma-separated required uids
            last_page_reached = False
            page_number = 1
            while not last_page_reached:
                fields = {
                    '_fields': 'uid,login',
                    'uid': uids_batch,
                    '_page': page_number,
                }
                uids_response = self._call_with_retry(
                    requests.get, self._staff_api_url, headers=self._headers, params=fields,
                )
                if not uids_response.ok:
                    logger.warning('%s request crashed', self._staff_api_url)
                    continue
                uids_json = uids_response.json()
                current_search_by_uid_result = uids_json.get('result', [])
                for user_entry in current_search_by_uid_result:
                    user_entry_id = user_entry['uid']
                    user_entry_login = user_entry['login']
                    uid_to_login[user_entry_id] = user_entry_login
                last_page_reached = (page_number >= uids_json.get('pages', 1))
                page_number += 1

        return uid_to_login

    def _check_achievement(self, login, correct_level):
        params = {
            'person.login': login,
            'achievement.id': self._achievement_id,
            '_fields': 'level,is_active',
        }
        url = f'{self._achievery_base_url}given/'
        result = requests.get(url, params=params, headers=self._headers, timeout=self._timeout)
        data = result.json()
        if data['total'] == 0:
            return UserStatus.without_achieve

        data = data['objects'][0]
        if not data['is_active']:
            return UserStatus.inactive

        if data['level'] < correct_level:
            return UserStatus.wrong_level

        return UserStatus.ok

    def _give_achievement(self, login, level=1):
        logger.info('Giving the achievement to user %s with the level=%s', login, level)
        if self._is_dry_run:
            return

        try:
            params = {
                'person.login': login,
                'achievement.id': self._achievement_id,
                'level': level,
            }
            url = f'{self._achievery_base_url}given/'
            requests.request(method='post', url=url, data=params, headers=self._headers, timeout=self._timeout)
        except Exception as err:
            logger.warning('Cannot give an achievement to %s: %s', login, err)

    def _edit_achievement(self, login, **kwargs):
        level = kwargs.get('level')
        logger.info(
            'Editing the achievement for user %s with the level %s and is_active=%s',
            login, level, kwargs.get('is_active', False)
        )
        if self._is_dry_run:
            return

        for key, value in kwargs.items():
            params_for_revision = {
                'person.login': login,
                'achievement.id': self._achievement_id,
                '_fields': 'id,revision,is_active',
            }
            url = f'{self._achievery_base_url}given/'
            try:
                response_get = requests.request(
                    method='get',
                    url=url,
                    params=params_for_revision,
                    headers=self._headers,
                    timeout=self._timeout,
                )
                dumped_data = response_get.json()['objects'][0]
                uid = dumped_data['id']
                revision = dumped_data['revision']
            except Exception as exc:
                logger.error('Cannot read parameters for editing %s: %s', login, exc)
                break

            data = {
                'revision': revision,
                key: value,
            }
            url = f'{self._achievery_base_url}given/{uid}/'
            put = requests.request(method='put', url=url, data=data, headers=self._headers, timeout=self._timeout)
            if not 200 <= put.status_code < 300:
                logger.warning(
                    'Cannot edit achievement for %s (set %s to %s), status = %s',
                    login, key, value, put.status_code
                )

    def handle(self):
        users = self._get_users()
        logger.info('Total amount of users to process: %s', len(users))

        for uid, user in users.items():
            try:
                correct_level = get_level(user['rated_score'])
                status = self._check_achievement(user['login'], correct_level)

                if correct_level == 0 and self._is_dry_run:
                    logger.info('User %s does not have enough marks (has only %s)', user['login'], user['rated_score'])

                elif correct_level == 0:
                    continue

                elif status == UserStatus.ok:
                    logger.info(
                        'User %s already has the achievement №%s of level %s',
                        user['login'], self._achievement_id, correct_level
                    )

                elif status == UserStatus.without_achieve:
                    self._give_achievement(user['login'], correct_level)

                elif status == UserStatus.wrong_level:
                    self._edit_achievement(user['login'], level=correct_level)

                elif status == UserStatus.inactive:
                    self._edit_achievement(user['login'], is_active=True, level=correct_level)

            except Exception as err:
                logger.warning('Cannot process %s: %s', user.get('login', 'no login'), err)
