import datetime

import requests
import json
import yaml
from retrying import retry
import socket
import os
import re
import asyncio
import itertools as itt
import warnings
from lp_queue.config import ENV_TYPE
from lp_queue.models import DefaultConfig, Task, Status
from sqlalchemy.exc import SQLAlchemyError
from lp_queue import db, app
from flask import current_app

warnings.filterwarnings("ignore")
DCs = 'sas', 'man', 'myt', 'iva', 'vla'


def _is_requests_error(exception: Exception):
    """
    used by retry decorator
    :param exception:
    :return: bool
    """
    return isinstance(exception, requests.exceptions.ConnectionError) \
        or isinstance(exception, requests.exceptions.Timeout)


@retry(retry_on_exception=_is_requests_error, stop_max_attempt_number=3)
def fetch_default_config(executor_type):
    database = DBCommunicator(db.session)
    default_config = DefaultConfig.query.filter_by(name=executor_type).first()

    default_config_url = 'https://github.yandex-team.ru/raw/load/lp_queue/master/lp_queue/util/default_configs/'
    default_config_url += f'{executor_type}.yaml'

    try:
        resp = requests.get(default_config_url)
        resp.raise_for_status()
    except requests.exceptions.HTTPError:
        if not default_config or not default_config.content:
            raise FileNotFoundError(f'no config for {executor_type}')
    else:
        if default_config:
            default_config.content = resp.content
            database.update(default_config)
        else:
            default_config = DefaultConfig(
                name=executor_type,
                content=resp.content,
            )
            database.create(default_config)

    return default_config.content


@retry(retry_on_exception=_is_requests_error, stop_max_attempt_number=3)
def validate_config(user_config, default_config):
    """
    не ловим исключения
    :param user_config: yaml
    :param default_config: из гитхаба. yaml
    :return:
    """
    # return {'success': True, 'error': '', 'config': user_config}

    result = {'success': False, 'error': '', 'config': None}
    validator_url = f'https://tank-validator{"-test" if ENV_TYPE != "production" else ""}' \
                    f'.common-int.yandex-team.ru/config/validate?fmt=yaml'
    resp = requests.post(validator_url, files={'user_config': ('user_config', user_config),
                                               'default_config': ('default_config', default_config)
                                               }, verify=False, timeout=1)
    resp.raise_for_status()
    validation = resp.json()
    assert validation.get('config'), 'validator never returned a config'
    if validation.get('errors'):
        result['error'] = f'CONFIG VALIDATION ERRORS: {validation["errors"]!r}'
    else:
        result['success'] = True
        result['config'] = validation['config']
    return result


class DCFinder:

    def get_dc(self, address):
        dcs = []
        try:
            assert address, f'address__is__{address!r}'
            host = self._get_fqdn(self._split_socket(address)[0])
            assert host, 'could not resolve hostname' % address
        except AssertionError as exc:
            app.logger.error(exc)
            return None
        except ValueError:
            app.logger.error('failed to parse socket %s', address)
            return None

        sources = [
            self.OOPSSource,
            self.TankPoolSource,
        ]

        # if re.match('.+\.gencfg-[a-z]\.yandex\.net', host) or re.match('.+\.search\.yandex\.net', host):
        #     sources.insert(0, self.GencfgSource)
        if re.match('.+\.qloud-[a-z]\.yandex\.net', host):
            sources.insert(0, self.QloudExtSource)
            sources.insert(0, self.QloudIntSource)

        @asyncio.coroutine
        @retry(retry_on_exception=_is_requests_error, stop_max_attempt_number=1)
        def coroutine(future, source):
            try:
                host_dc = yield from source(host).get_dc()
                assert host_dc in DCs, 'unknown dc %s' % host_dc
                future.set_result(host_dc)
            except (AssertionError, requests.exceptions.HTTPError) as e:
                app.logger.error(e)

        def got_result(future):
            dcs.append(future.result())

        loop = asyncio.get_event_loop()
        futures = [asyncio.Future() for _ in range(len(sources))]
        for f in futures:
            f.add_done_callback(got_result)
        tasks = itt.starmap(coroutine, zip(futures, sources))
        loop.run_until_complete(asyncio.wait(tasks))
        # loop.close()
        # TODO: что если в set больше одного элемента?
        return set(dcs).pop() if dcs else None

    class SourceInterface:
        s = requests.Session()
        s.verify = False
        s.timeout = 1
        url = ''
        headers = {}

        def __init__(self, host):
            self.host = host

        @asyncio.coroutine
        def get_dc(self):
            raise NotImplementedError

    class GolemSource(SourceInterface):
        url = 'http://ro.admin.yandex-team.ru/api/host_query.sbml?hostname=%s&columns=short_line'
        headers = {'Host': 'ro.admin.yandex-team.ru'}

        @asyncio.coroutine
        def get_dc(self):
            self.s.headers.update(self.headers)
            resp = self.s.get(url=self.url % self.host)
            resp.raise_for_status()
            assert resp
            host_dc = resp.text.strip()[:3]
            return host_dc

    class ConductorSource(SourceInterface):
        url = 'https://c.yandex-team.ru/api-cached/generator/get_dc?fqdn=%s'
        headers = {'Host': 'c.yandex-team.ru'}

        @asyncio.coroutine
        def get_dc(self):
            self.s.headers.update(self.headers)
            resp = self.s.get(url=self.url % self.host)
            resp.raise_for_status()
            assert resp
            host_dc = resp.text.strip()[:3]
            return host_dc

    class GencfgSource(SourceInterface):
        # admins refuse to open golem's 443 port for lunapark :(
        url = 'http://api.gencfg.yandex-team.ru/trunk/hosts_data'
        headers = {'Content-Type': 'application/json'}

        @asyncio.coroutine
        def get_dc(self):
            try:
                if self.host[:3] in DCs:
                    host_dc = self.host[:3]
                else:
                    self.s.headers.update(self.headers)
                    resp = self.s.post(url=self.url, data=json.dumps({'hosts': [self.host]}))
                    resp.raise_for_status()
                    assert resp.json().get('hosts_data') and self.host not in resp.json().get(u'notfound_hosts', []), \
                        'invalid gencfg response'
                    host_dc = resp.json()['hosts_data'][0].get('switch')[:3]
            except (requests.exceptions.ConnectionError, requests.exceptions.Timeout):
                app.logger.error("no connection to gencfg")
                host_dc = None
            except (requests.exceptions.RequestException, AssertionError):
                app.logger.error("gencfg doesn't know about %s", self.host)
                host_dc = None
            return host_dc

    class QloudIntSource(SourceInterface):
        url = 'https://qloud.yandex-team.ru/api/container/%s'
        token = os.environ.get('LUNAPARK_QLOUD_OAUTH_TOKEN')
        headers = {'Authorization': 'OAuth %s' % token, 'Host': 'qloud.yandex-team.ru'}

        @asyncio.coroutine
        def get_dc(self):
            self.s.headers.update(self.headers)
            resp = self.s.get(url=self.url % self.host)
            resp.raise_for_status()
            assert resp, 'invalid qloud response'
            host_dc = resp.json()['datacenter'].lower()
            return host_dc

    class QloudExtSource(SourceInterface):
        url = 'https://qloud-ext.yandex-team.ru/api/container/%s'
        token = os.environ.get('LUNAPARK_QLOUD_OAUTH_TOKEN')
        headers = {'Authorization': 'OAuth %s' % token, 'Host': 'qloud-ext.yandex-team.ru'}

        @asyncio.coroutine
        def get_dc(self):
            self.s.headers.update(self.headers)
            resp = self.s.get(url=self.url % self.host)
            resp.raise_for_status()
            assert resp, 'invalid qloud response'
            host_dc = resp.json()['datacenter'].lower()
            return host_dc

    class OOPSSource(SourceInterface):
        url = 'https://oops.yandex-team.ru/api/hosts/%s/attributes/location'
        headers = {'Host': 'oops.yandex-team.ru'}

        @asyncio.coroutine
        def get_dc(self):
            self.s.headers.update(self.headers)
            resp = self.s.get(url=self.url % self.host)
            resp.raise_for_status()
            assert resp, 'invalid oops response'
            host_dc = resp.json()['city'].lower()
            return host_dc

    class TankPoolSource(SourceInterface):
        @property
        def _dc_map(self):
            # try:
            #     url = 'https://c.yandex-team.ru/api-cached/generator/lunapark_tanks'
            #     resp = self.s.get(url=url)
            #     resp = resp.json()  # [[host, dc, spec], [host, dc, spec], ...]
            #     tanks = {
            #         'fol': (),
            #         'sas': tuple([str(tank[0])+'.yandex.net' for tank in resp if tank[1] == 'sas']),
            #         'myt': tuple([str(tank[0])+'.yandex.net' for tank in resp if tank[1] == 'myt']),
            #         'iva': tuple([str(tank[0])+'.yandex.net' for tank in resp if tank[1] == 'iva']),
            #         'man': tuple([str(tank[0])+'.yandex.net' for tank in resp if tank[1] == 'man']),
            #         'ugr': tuple([str(tank[0])+'.yandex.net' for tank in resp if tank[1] == 'ugr']),
            #         'vla': (),
            #     }
            #     assert any(tanks.values())
            # except AssertionError:
            #     logging.exception('Could not get lunapark tanks from conductor due to:')
            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', 'fobos.tanks.yandex.net', 'kv1.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

        @asyncio.coroutine
        def get_dc(self):
            for k, vv in self._dc_map.items():
                for v in vv:
                   if v == self.host:
                       return k

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

    @staticmethod
    def _split_socket(address: str):
        """
        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 = 80
        except:
            app.logger.exception('')
            raise ValueError
        return host, port


# def check_tank_dc(tank_name: str, tank_dc: str):
#     tank_dc_check = DCFinder().get_dc(tank_name)
#     if not tank_dc_check:
#         logging.error('have to beleive tank %s that he is in %s', tank_name, tank_dc)
#         return tank_dc
#     elif tank_dc_check != tank_dc:
#         logging.error('tank %s is lying! he is in %s not in %s', tank_name, tank_dc_check, tank_dc)
#         return tank_dc_check
#     else:
#         return tank_dc


class DBCommunicator:
    """
    for humans
    """

    def __init__(self, db_session):
        """

        :param db_session: sqlAlchemy db.session
        """
        self.session = db_session

    def _is_sql_alchemy_error(exception: Exception):
        """
        used by retry decorator
        to deal with sql alchemy reconnects and rollbacks
        :return:
        """
        return isinstance(exception, SQLAlchemyError)

    @retry(retry_on_exception=_is_sql_alchemy_error, stop_max_attempt_number=1)
    def create(self, what):
        try:
            self.session.add(what)
            self.session.commit()
        except SQLAlchemyError as sqlexc:
            app.logger.error(repr(sqlexc))
            self.session.rollback()
            raise

    @retry(retry_on_exception=_is_sql_alchemy_error, stop_max_attempt_number=1)
    def update(self, what):
        try:
            self.session.merge(what)
            self.session.commit()
        except SQLAlchemyError as sqlexc:
            app.logger.error(repr(sqlexc))
            self.session.rollback()
            raise


def claim_task(task: Task, executor_id):
    """
    assigns task to tank, changes its status
    takes locks from task.demands
    :param task:
    :param executor_id:
    :return:
    """
    database = DBCommunicator(db.session)
    # # check locks
    # to_lock = task.demands.get('lock', [])
    # assert not Lock.query.filter(Lock.what.in_(to_lock)), f'there\'s lock for {to_lock!r}'
    # # take locks
    # for l in to_lock:
    #     lock = Lock(
    #         what=l
    #     )
    #     database.create(lock)

    # claim task
    task.claimed_by = executor_id
    task.status = Status.CLAIMED
    task.claimed_at = datetime.datetime.utcnow()
    database.update(task)


class Formatter:
    def __init__(self, fmt):
        if fmt == 'yaml':
            self.load = yaml.load
            self.dump = yaml.dump
        else:
            self.load = json.loads
            self.dump = json.dumps
