# -*- coding: utf-8 -*-
"""
Created on Apr 1, 2015

@author: noob
"""

from common.util.decorators import memoized_property, CacheClient
from datetime import datetime
import logging
import requests
import socket
from itertools import chain
import json
import re
import settings

PRESTABLE_TANKS = 'buratino.tanks.yandex.net', 'centurion.tanks.yandex.net', 'cromwell.tanks.yandex.net',

QLOUD_TOKEN = settings.OAUTH_TOKEN
PLATFORM_TOKEN = settings.PLATFORM_TOKEN

# admins refuse to open golem's 443 port for lunapark :(
GOLEM_SOURCE = {
    'url': settings.GOLEM_SOURCE_URL,
    'headers': {'Host': 'ro.admin.yandex-team.ru'},
    'verify': True
}
CONDUCTOR_SOURCE = {
    'url': settings.CONDUCTOR_SOURCE_URL,
    'headers': {'Host': 'c.yandex-team.ru'},
    'verify': True
}

QLOUD_INT_SOURCE = {
    'url': settings.QLOUD_INT_SOURCE_URL,
    'verify': False,  # '/usr/share/yandex-internal-root-ca/YandexInternalRootCA.crt',
    'headers': {'Authorization': 'OAuth {}'.format(QLOUD_TOKEN),
                'Host': 'qloud.yandex-team.ru'},
}
QLOUD_EXT_SOURCE = {
    'url': settings.QLOUD_EXT_SOURCE_URL,
    'verify': True,
    'headers': {'Authorization': 'OAuth {}'.format(QLOUD_TOKEN),
                'Host': 'qloud-ext.yandex-team.ru'},
}

PLATFORM_SOURCE = {
    'url': settings.PLATFORM_SOURCE_URL,
    'verify': True,
    'headers': {'Authorization': 'OAuth {}'.format(PLATFORM_TOKEN),
                'Host': 'qloud-ext.yandex-team.ru'},
}


class TankStatusError(Exception):
    pass


class TankConnectionError(Exception):
    pass


class TankApiError(Exception):
    pass


class TankFinder(object):
    """
    Checks target validity.
    Finds free tank in the same DC as target is.
    """

    def __init__(self, target, use_tank: str = None, use_tank_port=None):
        """

        :param target:
        """
        self.target = target
        self.use_tank = use_tank
        self.use_tank_port = use_tank_port
        self.use_conductor_group = None
        self.use_nanny = None

        if self.use_tank and self.use_tank.lower().startswith('conductor:'):
            self.use_conductor_group = self.use_tank.split(':', 1)[1]
        elif self.use_tank and self.use_tank.lower().startswith('nanny:'):
            self.use_nanny = self.use_tank.split(':', 1)[1]

        self.ENV_TYPE = settings.ENV_TYPE

    def tank(self):
        """
        main method
        """
        success = False
        error = ''
        error_link = ''
        try:
            target = self.approve_target()
            success, error, error_link = target['success'], target['error'], target.get('error_link')
            assert success
            target_dc = target['target_dc']
            tank = self.find_tank(target_dc)
            success, error, error_link = tank['success'], tank['error'], tank['error_link']
            assert success
        except AssertionError:
            tank = {'success': success, 'error': error, 'error_link': error_link}
        return tank

    def approve_target(self, check_lock=True):
        success = False
        error = ''
        error_link = ''  # i.e. for unlocking target link
        target_dc = ''

        try:
            target_host, target_port = self._split_socket(self.target)
            target_fqdn = self._get_fqdn(target_host)
            target_dc = self._get_dc(target_fqdn)
            if not target_dc:
                raise KeyError
            if not self.use_tank:
                self.dc_map[target_dc]
            if check_lock:
                target_availability = self._check_target_lock(target_fqdn)
                assert target_availability['success']
            success = True
        except ValueError:
            error = 'Неправильный формат сокета в поле address: {}'.format(self.target)
        except KeyError:
            error = 'Не удалось определить ДЦ, в котором живет мишень. Вы точно не в балансер стреляете?'
            error_link = 'https://neverov.at.yandex-team.ru/986'
        except AssertionError:
            error = target_availability['error']
            error_link = target_availability.get('error_link', '')
        except Exception as e:
            logging.error('Problems with approve target', exc_info=True)
            error = repr(e)
        return {'success': success, 'error': error, 'error_link': error_link, 'target_dc': target_dc}

    def find_tank(self, target_dc):
        """
        Check if the tank is not busy
        Check if tank in use_tank option and specified target are in one datacenter
        :param target_dc: string
        """
        tank_fqdn = None
        tank_port = 8083
        tank_is_available = False
        error = ''
        error_link = ''
        # TODO: allow using group and fqdn in one list;
        # CONDUCTOR
        if self.use_conductor_group:
            try:
                logging.error('Using conductor group {}'.format(self.use_conductor_group))
                c_url = 'https://c.yandex-team.ru/api-cached/generator/get_dc_from_group?group={}'\
                        .format(self.use_conductor_group)
                group_hosts = requests.get(c_url, headers={'Host': 'c.yandex-team.ru'}, timeout=5)
                group_hosts.raise_for_status()
                assert not group_hosts.content.startswith(b'<html>'), 'В этой группе нет хостов.'
                group_hosts = [h for h in group_hosts.content.split('\n') if h]
                assert group_hosts, 'В этой группе нет хостов.'
                group_hosts = {dc: [h.split(' ')[0] for h in group_hosts if h.split(' ')[1] == dc] for dc in
                               list(set(h.split(' ')[1] for h in group_hosts))}
                # Ищем свободные танки
                assert target_dc in list(group_hosts.keys()), 'Нет танков в {}'.format(target_dc)
                for tank_address in group_hosts[target_dc]:
                    try:
                        tank_host, tank_port = self._split_socket(tank_address)
                        if self.use_tank_port:
                            tank_port = self.use_tank_port
                        tank_fqdn = self._get_fqdn(tank_host)
                        tank_is_available = self._check_tank_status(tank_fqdn, tank_port)
                        if tank_is_available:
                            success = True
                            break
                    except (ValueError, TankConnectionError, TankApiError):
                        continue
                assert tank_is_available, 'К сожалению, свободных танков в {} сейчас нет.'.format(target_dc)
            except requests.RequestException:
                error = 'Нет связи с кондуктором.'
            except AssertionError as aexc:
                error = aexc.args[0]
            except Exception as exc:
                logging.error('Problems with tank_finder', exc_info=True)
                error = repr(exc)

        # NANNY
        elif self.use_nanny:
            try:
                class Nanny(object):
                    host = 'nanny.yandex-team.ru'

                    def __init__(self, service, gencfg_group='ALL_RCLOUD_TANKS'):
                        self.service = service
                        self.gencfg_group = gencfg_group.upper()

                    @property
                    def url(self):
                        return 'https://{}/v2/services/{}/current_state/instances/'.format(self.host, self.service)

                nanny = Nanny(*self.use_nanny.split(':'))
                logging.error('Using nanny service {} and gencfg group {}'.format(nanny.service, nanny.gencfg_group))
                group_hosts = requests.get(nanny.url, headers={'Host': nanny.host}, timeout=2, verify=False)
                group_hosts.raise_for_status()
                assert not group_hosts.content.startswith(b'<html>'), 'Не удалось получить информацию из няни'
                group_hosts = [h for h in group_hosts.json()['result'] if h]
                assert group_hosts, 'Не удалось получить информацию из няни'
                # фильтруем по генцфг группе
                group_hosts = [h for h in group_hosts
                               if 'a_topology_group-{}'.format(nanny.gencfg_group) in h.get('itags', [])]
                assert group_hosts, \
                    'Не нашлось танков в сервисе {} и группе {}'.format(nanny.service, nanny.gencfg_group)
                group_hosts = {dc: ['{}:{}'.format(h['container_hostname'], h['port']) for h in group_hosts if
                                    h['container_hostname'].split('-')[0][:3] == dc] for dc in
                               list(set(h['container_hostname'].split('-')[0][:3] for h in group_hosts))}
                # Ищем свободные танки
                assert target_dc in list(group_hosts.keys()), 'Нет танков в {}'.format(target_dc)
                for tank_address in group_hosts[target_dc]:
                    try:
                        tank_host, tank_port = self._split_socket(tank_address)
                        if self.use_tank_port:
                            tank_port = self.use_tank_port
                        tank_fqdn = self._get_fqdn(tank_host)
                        tank_is_available = self._check_tank_status(tank_fqdn, tank_port)
                        if tank_is_available:
                            break
                    except (ValueError, TankConnectionError, TankApiError):
                        continue
                assert tank_is_available, 'К сожалению, свободных танков в {} сейчас нет.'.format(target_dc)
            except requests.RequestException:
                error = 'Нет связи с няней.'
            except AssertionError as aexc:
                error = aexc.args[0]
            except Exception as exc:
                logging.error('Problems with tank finder', exc_info=True)
                error = repr(exc)

        # SINGLE TANK
        elif self.use_tank:
            try:
                tank_host, tank_port = self._split_socket(self.use_tank)
                if self.use_tank_port:
                    tank_port = self.use_tank_port
                tank_fqdn = self._get_fqdn(tank_host)
                # Проверяем, не наш ли это танк, чтобы лишний раз не ходить в кондуктор и голем за ДЦ
                if tank_fqdn in chain.from_iterable(list(self.dc_map.values())):
                    tank_dc = [k for k in list(self.dc_map.keys()) if tank_fqdn in self.dc_map[k]][0]
                else:
                    tank_dc = self._get_dc(tank_fqdn)
                if not target_dc:
                    error_link = 'https://neverov.at.yandex-team.ru/986'
                    raise AssertionError(
                        'Не удалось определить ДЦ, в котором живет мишень. Вы точно не в балансер стреляете?'
                    )
                assert tank_dc, 'Не удалось определить ДЦ, в котором живет танк.'
                assert tank_dc == target_dc, 'Танк и мишень находятся в разных датацентрах. ' \
                                             'Танк в {}, Мишень в {}.'.format(tank_dc, target_dc)
                tank_is_available = self._check_tank_status(tank_fqdn, tank_port)
                if not tank_is_available:
                    raise TankStatusError()
            except ValueError:
                error = 'Неправильный формат сокета в поле use_tank: {}'.format(self.use_tank)
            except AssertionError as aexc:
                error = aexc.args[0]
            except TankConnectionError:
                error = 'Нет связи с танком {}'.format(self.use_tank)
            except TankStatusError:
                error = 'Танк {} занят'.format(self.use_tank)
            except TankApiError as taexc:
                error = taexc.args[0]
            except Exception as e:
                error = repr(e)
        else:
            tanks_in_dc = self.dc_map[target_dc]
            # Ищем свободные танки
            for tank_address in tanks_in_dc:
                try:
                    tank_host, tank_port = self._split_socket(tank_address)
                    if self.use_tank_port:
                        tank_port = self.use_tank_port
                    tank_fqdn = self._get_fqdn(tank_host)
                    tank_is_available = self._check_tank_status(tank_fqdn, tank_port)
                    if tank_is_available:
                        break
                except (ValueError, TankConnectionError):
                    continue
            if not tank_is_available:
                error = 'К сожалению, доступных танков в {} сейчас нет.'.format(target_dc)
        return {
            'success': tank_is_available,
            'fqdn': tank_fqdn,
            'port': tank_port,
            'error': error,
            'error_link': error_link
        }

    # TODO: retries
    def _check_tank_status(self, tank_fqdn, tank_port):
        tankapi_status_url = 'http://{}:{}/api/v1/tank/status.json'.format(tank_fqdn, tank_port)
        try:
            tank_status = requests.get(tankapi_status_url).json()
            assert tank_status.get('success', False)
        except requests.exceptions.ConnectionError:
            raise TankConnectionError()
        except AssertionError:
            logging.error('Tankapi Error: %s', tank_status.get('error', ''))
            raise TankApiError(tank_status.get('error', ''))
        # Если кто-то засел на танке и сидит там без дела больше 30 минут, то танк юзаем.
        now = datetime.now()
        try:
            assert not self.use_tank and self.ENV_TYPE not in ('testing', 'development')
            recently_active_users = any(
                'days' not in list(u.values())[0]
                and 0 < int(now.hour * 60 + now.minute -
                            float(list(u.values())[0].split(':')[0].replace('s', '')) * 60 +
                            float(list(u.values())[0].split(':')[1].replace('m', ''))
                            ) < 30
                for u in tank_status['users_activity']
            )
        except AssertionError:
            recently_active_users = False
        except:
            logging.error('Problems with check tank status', exc_info=True)
            recently_active_users = True
        tank_free = bool(
            not tank_status['is_preparing'] and not tank_status['is_testing'] and not recently_active_users)
        return tank_free

    @memoized_property
    def dc_map(self):
        """
        Only non-special tanks for each datacenter
        """
        tanks = {
            'sas': ('t18.cloud.load.yandex.net', 't26.cloud.load.yandex.net'),
            'myt': (),
            'iva': (),
            'man': (),
            'ugr': (),
            'vla': (),
        }
        try:
            url = 'https://c.yandex-team.ru/api-cached/generator/lunapark_tanks'
            resp = requests.get(url, verify=False)
            resp = resp.json()  # [[host, dc, spec], [host, dc, spec], ...]
            tanks = {
                'fol': (),
                'sas': tuple('{}.yandex.net'.format(tank[0]) for tank in resp if tank[1] == 'sas' and not tank[2]),
                'myt': tuple('{}.yandex.net'.format(tank[0]) for tank in resp if tank[1] == 'myt' and not tank[2]),
                'iva': tuple('{}.yandex.net'.format(tank[0]) for tank in resp if tank[1] == 'iva' and not tank[2]),
                'man': tuple('{}.yandex.net'.format(tank[0]) for tank in resp if tank[1] == 'man' and not tank[2]),
                'ugr': tuple('{}.yandex.net'.format(tank[0]) for tank in resp if tank[1] == 'ugr' and not tank[2]),
                'vla': (),
            }
            assert any(tanks.values())
        except:
            logging.error('Could not get lunapark tanks from conductor due to:', exc_info=True)
            tanks = {
                'fol': (),
                'sas': ('wolverine.tanks.yandex.net', 'chaffee.tanks.yandex.net', 'stuart.tanks.yandex.net',
                        'centurion.tanks.yandex.net', 'hellcat.tanks.yandex.net', 'lee.tanks.yandex.net',
                        'scorpion.tanks.yandex.net', 'vickers.tanks.yandex.net'),
                'myt': ('buratino.tanks.yandex.net', 'bumblebee.tanks.yandex.net', 'yenisey.tanks.yandex.net',
                        'steam.tanks.yandex.net'),
                'iva': ('peony.tanks.yandex.net', 'tulip.tanks.yandex.net', 'violet.tanks.yandex.net'),
                'man': ('cromwell.tanks.yandex.net', 'abrams.tanks.yandex.net', 'comet.tanks.yandex.net'),
                'ugr': (),
                'vla': (),
            }
        return tanks

    def _get_dc(self, host):
        """
        Getting datacenter for a tank or target from memcached or directly from Conductor, Golem or Qloud
        """

        host_dc = CacheClient().get('server_{}_dc'.format(host))

        if not host_dc:
            if re.match(r'.+\.yp-[a-z]\.yandex\.net', host):  # <pod_id>.<dc>.yp-c.yandex.net
                try:
                    host_dc = host.split('.')[-4]
                except IndexError:
                    host_dc = ''
            elif self.use_nanny or re.match(r'.+\.gencfg-[a-z]\.yandex\.net', host):
                try:
                    if host[:3] in list(self.dc_map.keys()):
                        host_dc = host[:3]
                    else:
                        resp = requests.post(settings.GENCFG_API_URL,
                                             headers={'Content-Type': 'application/json'},
                                             data=json.dumps({'hosts': [host]}),
                                             )
                        resp.raise_for_status()
                        assert resp.json().get('hosts_data') and host not in resp.json().get('notfound_hosts', [])
                        host_dc = resp.json()['hosts_data'][0].get('switch')[:3]
                except (requests.exceptions.ConnectionError, requests.exceptions.Timeout):
                    logging.error('no connection to gencfg')
                except (requests.exceptions.RequestException, AssertionError):
                    logging.error("gencfg doesn't know about %s", host)
                    host_dc = ''
            else:
                sources = [
                    CONDUCTOR_SOURCE,
                    GOLEM_SOURCE,
                ]

                if re.match(r'.+\.qloud-[a-z]\.yandex\.net', host):  # чтобы сразу идти в кулауд
                    sources.insert(0, QLOUD_EXT_SOURCE)
                    sources.insert(0, QLOUD_INT_SOURCE)
                    sources.insert(0, PLATFORM_SOURCE)
                else:
                    sources += [PLATFORM_SOURCE, QLOUD_INT_SOURCE, QLOUD_EXT_SOURCE]

                for source in sources:
                    try:
                        resp = requests.get(source['url'].format(host),
                                            headers=source['headers'],
                                            timeout=5,
                                            verify=source['verify'])
                        resp.raise_for_status()
                        assert resp
                        try:
                            host_dc = resp.json()['datacenter'].lower()
                        except:
                            host_dc = resp.text.strip()[:3]
                        if not self.use_tank:
                            assert host_dc in list(self.dc_map.keys())
                        logging.debug('found {} in {}: {}'.format(host, source['headers']['Host'], host_dc))
                        break
                    except (requests.exceptions.RequestException, AssertionError):
                        logging.warning("%s doesn't know about %s", source['headers']['Host'], host)
                        host_dc = ''
                        continue
        return host_dc

    @staticmethod
    def _get_fqdn(host):
        try:
            fqdn = socket.getfqdn(host)
        except (socket.gaierror, socket.herror):
            logging.exception('')
            # FIXME: remove this try when GENCFG-1448 is resolved
            host += '.yandex.net'
            try:
                fqdn = socket.getfqdn(host)
            except (socket.gaierror, socket.herror):
                logging.exception('')
                fqdn = host
        return fqdn

    @staticmethod
    def _split_socket(address):
        """
        supported address inputs:
        'ipv4',
        'ipv4:port',
        '[ipv4]:port',
        'ipv6',
        '[ipv6]:port',
        'host',
        'host:port',
        '[host]:port'
        """
        try:
            assert address
            ipv6 = len(address.split(':')) > 2
            if address.find(']:') != -1:
                # [ipv4]:port
                # [ipv6]:port
                # [host]:port
                host = ':'.join(address.split(':')[:-1]).replace('[', '').replace(']', '')
                port = int(address.split(':')[-1])
            elif address.find(':') != -1 and not ipv6:
                # ipv4:port
                # host:port
                host = address.split(':')[0].replace('[', '').replace(']', '')
                port = int(address.split(':')[-1])
            else:
                # ipv4
                # ipv6
                # host
                host = address
                port = 8083
        except:
            logging.exception('')
            raise ValueError
        return host, port

    def _check_target_lock(self, target_fqdn):
        """
        unlocks if locked for 'None' job
        :param target_fqdn: target host
        """
        jobno = None
        success = False
        error = ''
        error_link = ''

        if self.ENV_TYPE == 'production':
            api_address = 'lunapark.yandex-team.ru'
            verify = True
        else:
            api_address = 'lunapark-develop.in.yandex-team.ru'
            verify = False  # '/usr/share/yandex-internal-root-ca/YandexInternalRootCA.crt'

        try:
            uri = 'https://{}/api/server/lock.json/?action=check&address={}'.format(api_address, target_fqdn)
            resp = requests.get(uri, verify=verify, headers={'host': api_address})

            resp = json.loads(resp.content.decode('utf-8'))[0]
            if resp['status'] == 'locked':
                # Пытаемся разблокировать
                error = 'Мишень заблокирована, чтобы разблокировать используйте ссылку:'
                error_link = 'https://{}/api/server/lock.json/?action=unlock&address={}'.format(api_address, target_fqdn)
            else:
                success = True
        except:
            logging.error('Could not check target lock for host %s due to ', target_fqdn, exc_info=True)
            success = True  # screw locks
            error = ''
        finally:
            return {'success': success, 'jobno': jobno, 'error': error, 'error_link': error_link}
