import time
import json
import os
import uuid
import random
from tempfile import NamedTemporaryFile
try:
    from urllib.error import URLError
except ImportError:
    from urllib2 import URLError

from redis.exceptions import RedisError
from werkzeug.exceptions import ServiceUnavailable
from requests.exceptions import ConnectionError

try:
    from load.projects.tankapi_cmd.src.client import LunaparkTank, TestPerformer
except ModuleNotFoundError:
    from tankapi.client.client import LunaparkTank, TestPerformer

try:
    from config import logger, redis_master, redis_slave, TASK_LIFESPAN
    from statuses import InternalStatus, STATUS_MAP
    from external_calls import call_tank_finder, call_validator
    from utils import get_tanks_from_config, get_target_from_config, rewrite_target_in_config
except ModuleNotFoundError:
    from .config import logger, redis_master, redis_slave, TASK_LIFESPAN
    from .statuses import InternalStatus, STATUS_MAP
    from .external_calls import call_tank_finder, call_validator
    from .utils import get_tanks_from_config, get_target_from_config, rewrite_target_in_config


class IdNotFound(Exception):
    pass


class Task:
    """
    Basic task for testflow
    """

    def __init__(self, config):
        self.id = uuid.uuid4().hex
        self.status = InternalStatus.NEW
        self.errors = {}

        self.task_record = TaskRecord(task_id=self.id, status=self.status, config=config)
        self.test = TankTest()

        self.tanks = [LunaparkTank(), ]
        logger.info('Task %s created', self.id)

    def _fail_task(self, target_status, errors, section='firestarter'):
        self.status = target_status
        for error in errors:
            self.errors.setdefault(section, error)
            logger.warning('Task id %s got an error from tank: %s', self.id, error)
        try:
            self.task_record.write(self.status, self.errors)
        except RedisError:
            logger.error('Problems with Redis while failing task %s', self.id)

    def validate_config(self):
        """ Validates config in validator """
        logger.info('Validate config for task %s', self.id)
        try:
            config = json.loads(self.task_record.read('config')[0].decode('utf-8'))
            validated_config, errors = call_validator(config)
            self.status = InternalStatus.VALIDATED
            if errors:
                self._fail_task(InternalStatus.FAILED_TO_START, errors, section='validator')
            self.task_record.write(self.status, self.errors, validated_config=json.dumps(validated_config))
        except RedisError:
            self._fail_task(InternalStatus.FAILED_TO_START, ['Problems with Redis'])

    def define_hosts(self):
        """ Define apppropriate tank and target """
        logger.info('Define tanks and target for task %s', self.id)
        try:
            config = json.loads(self.task_record.read('validated_config')[0].decode('utf-8'))
            config_tank, config_tank_port = get_tanks_from_config(config)
            config_target, config_target_port = get_target_from_config(config)
            if not config_target or not config_target_port:
                self._fail_task(InternalStatus.FAILED_TO_START, ['Failed to parse target and port from config'])
                return

            target_found, target_port_found, tanks_found, error = call_tank_finder(config_tank, config_tank_port,
                                                                                   config_target, config_target_port)
            if config_target.startswith('nanny:'):
                config = rewrite_target_in_config(config, target_found, target_port_found)

            if error:
                fail_reason = error if error else 'Tank and target are not in the same DC, sorry'
                self._fail_task(InternalStatus.FAILED_TO_START, [fail_reason], section='tank_finder')
                return

            self.tanks = random.shuffle(tanks_found)
            self.status = InternalStatus.TANK_FOUND
            logger.debug('Tanks for task %s: %s. Target: %s:%s', self.id,
                         ', '.join([host.host for host in self.tanks]),
                         target_found, target_port_found)
            self.task_record.write(self.status, self.errors, validated_config=json.dumps(config))

        except RedisError:
            self._fail_task(InternalStatus.FAILED_TO_START, ['Problems with Redis'])

    def start_test(self):
        """ Start test on one of available tank """

        logger.info('Start test for task %s', self.id)
        config = self.task_record.read('validated_config')[0]

        if not config:
            self.task_record.write(self.status, self.errors)
            return

        performer = TestInitiator()

        performer.api_clients = performer.check_available(self.tanks)
        if not performer.api_clients:
            self._fail_task(InternalStatus.FAILED_TO_START,
                            ['Tanks {} are not available'.format(','.join([tank.host for tank in self.tanks]))])
            return

        with NamedTemporaryFile(delete=False) as config_file:
            config_file.write(config)
        performer.start_tests([config_file.name], None, None, None, None, None)
        for client in performer.api_clients:
            if client.test_id:
                self.status = InternalStatus.INITIATED
                self.test.started = True
                self.test.tank = client
                break
        if not self.test.started:
            self._fail_task(InternalStatus.FAILED_TO_START, ['Failed to start test on tanks'])
        os.unlink(config_file.name)
        self.task_record.write(self.status, self.errors, test_id=self.test.tank.test_id,
                               tank_host=self.test.get_tank_connection_string())

    def check_test_status(self):
        logger.info('Check status for task %s', self.id)
        luna_id, test_status, errors = self.test.check_status()

        if test_status == 'FINISHED' and not luna_id:
            logger.info('No luna id found for task %s. Test %s on tank %s',
                         self.id, self.test.tank.test_id, self.test.tank.host)
            errors = 'Test was performed but results are not uploaded to Luna.\n' + \
                     'Tank {}, local test id {}'.format(self.test.tank.host, self.test.tank.test_id)

        if errors:
            self._fail_task(InternalStatus.FAILED, [errors], section='tankapi')
        elif test_status:
            self.status = test_status.lower()
            self.task_record.write(self.status, self.errors, luna_id=luna_id)

        logger.info('Task %s status %s', self.id, self.status)


class TaskRecord:
    """
    Task record in Redis
    """

    def __init__(self, task_id, status, config, create_mode=True):
        self.redis_master = redis_master
        self.redis_slave = redis_slave

        self.id = task_id
        if create_mode:
            self.create(status, config)

    def create(self, status, config):
        self.write(task_status=status, task_errors='', config=config)
        self.redis_master.expire(self.id, TASK_LIFESPAN)
        logger.info('Create record for task %s', self.id)

    def read(self, *args):
        try:
            return self.redis_slave.hmget(self.id, *args)
        except (RedisError, ConnectionError):
            logger.error('Failed to read task %s data from Redis', self.id, exc_info=True)
            raise RedisError

    def write(self, task_status, task_errors, **kwargs):
        record = {'status': task_status, 'errors': json.dumps(task_errors)}
        record.update(kwargs)
        try:
            self.redis_master.hmset(self.id, record)
        except (RedisError, ConnectionError):
            logger.error('Failed to write task %s data to Redis: %s', self.id, record, exc_info=True)
            raise RedisError


class TestInitiator(TestPerformer):
    def __init__(self):
        try:
            super().__init__()
        except TypeError:
            TestPerformer.__init__(self)
        self.retry_interval = 5
        self.tank_attempts = 3

    def choose_available_tank(self, candidates, **kwargs):
        for _ in range(self.tank_attempts):
            # noinspection PyAttributeOutsideInit
            self.api_clients = self.check_available(candidates)
            if not self.api_clients:
                logger.debug("Retry tanks check in %s sec...", self.retry_interval)
                time.sleep(self.retry_interval)
            else:
                break

    @staticmethod
    def check_available(candidates):
        available = []
        for client in candidates:
            logger.info('Check tank %s...', client.host)
            try:
                res = client.get_status()
            except Exception:
                res = {'success': False}
            if res['success'] and not res['is_testing']:
                logger.debug("%s is available for test", client.host)
                available.append(client)
                break
            logger.info('Tank %s is not available', client.host)
            logger.debug('Status of %s:\n%s', client.host, res)
        return available


class TankTest:
    def __init__(self):
        self.started = False
        self.tank = LunaparkTank()

    def check_status(self):
        current_status = {}
        try:
            current_status = self.tank.get_test_status(self.tank.test_id)
        except (ConnectionError, URLError):
            logger.warning('Impossible to connect to tank %s', self.tank.host)
        luna_id = current_status.get('luna_id', '')
        luna_id = luna_id if luna_id else ''
        test_status = current_status.get('status_code')
        # TODO: https://st.yandex-team.ru/YANDEXTANK-328
        errors = current_status.get('error') or current_status.get('tank_msg')
        return luna_id, test_status, errors

    def get_tank_connection_string(self):
        return '{host}:{port}'.format(host=self.tank.host, port=self.tank.port)


def reply_on_start(firestarter_id):
    try:
        task_record = TaskRecord(task_id=firestarter_id, status='', config={}, create_mode=False)
        config = json.loads(task_record.read('config')[0].decode('utf-8'))
    except ValueError:
        logger.error('Impossible to load config from Redis', exc_info=True)
        config = {}
    return {'firestarter_id': firestarter_id, 'config': config}


def reply_on_status(firestarter_id):
    try:
        task_record = TaskRecord(task_id=firestarter_id, status='', config={}, create_mode=False)
        data = task_record.read('luna_id', 'status', 'config', 'errors')
        luna_id = data[0].decode('utf-8') if data[0] else None
        status = STATUS_MAP[data[1].decode('utf-8')]
        config = json.loads(data[2].decode('utf-8'))
        errors = json.loads(data[3].decode('utf-8'))
    except RedisError:
        logger.error('Redis problem', exc_info=True)
        raise ServiceUnavailable
    except (KeyError, ValueError, IndexError, AttributeError):
        logger.error('Task record %s does not contain requested data', firestarter_id, exc_info=True)
        raise IdNotFound

    result = {
        'firestarter_id': firestarter_id,
        'luna_id': luna_id,
        'status': status,
        'config': config,
        'errors': errors,
        'internal_status': data[1].decode('utf-8')
        }
    logger.debug('Status reply: %s', result)
    return result, status
