# -*- coding: utf-8 -*-

"""
    Python-обёртка для работы с Lunapark Tank API
    Работает с API танка через json формат
"""

import datetime
import logging
import mimetypes
import os
import time
import socket
import traceback
import fnmatch
import platform
import requests
import sys
from retrying import retry
try:
    from version import VERSION
except ImportError:
    from .version import VERSION

try:
    from urllib.request import urlopen
except ImportError:
    from urllib2 import urlopen

try:
    import simplejson as json
except ImportError:
    import json


class TestPerformer:
    def __init__(self):
        self.retry_interval = 5
        self.api_clients = []
        self.reported_web_link = False

    def __check_tank_availability(self, candidates, require_all):
        available = []
        for client in candidates:
            # get remote status
            try:
                res = client.get_status()
            except Exception as e:
                logging.debug("%s", traceback.format_exc(e))
                logging.warning("Error checking status %s: %s", client.host, e)
                res = {'success': False}

            # analyse status
            if res['success'] and not res['is_testing']:
                logging.info("%s is available for test", client.host)
                available.append(client)
                if not require_all:
                    break
            else:
                if res['success'] and res.get('left_time'):
                    logging.info("%s is not available, ETA %s", client.host,
                                 datetime.timedelta(seconds=res['left_time']))
                else:
                    logging.info("%s is not available", client.host)

                if require_all:
                    return []
        return available

    def choose_available_tank(self, candidates, require_all=False):
        if require_all:
            logging.info("Multi-tank test mode, waiting for all tanks to be available")

        while True:
            self.api_clients = self.__check_tank_availability(candidates, require_all)
            if not self.api_clients:
                logging.info("Retry in %s sec...", self.retry_interval)
                time.sleep(self.retry_interval)
            else:
                break

    def start_tests(self, config, ammo, files, jobno_file, cli_options, cfg_patches):
        # start tests
        for api_client in self.api_clients:
            res = api_client.start_test(config, ammo, files, cli_options, cfg_patches)
            if not res.get('success'):
                raise RuntimeError(res['error'])
            api_client.test_id = res['id']
            logging.info("Got test ID at %s: %s", api_client.host, api_client.test_id)
            if len(self.api_clients) > 1:
                self.save_status(jobno_file, "meta", "")
            else:
                self.save_status(jobno_file, api_client.test_id, "")

    def perform(self, config, ammo, files, jobno_file, download_list, cli_options, cfg_patches):
        retcode = 1
        try:
            self.start_tests(config, ammo, files, jobno_file, cli_options, cfg_patches)
            retcode = self.wait(jobno_file) or 0
        except KeyboardInterrupt:
            logging.info("Interrupted.")
            self.shutdown_tests()
        except InterruptedTestException as exc:
            logging.warning("The test was interrupted: ", exc_info=True)
            retcode = exc.retcode
        except Exception:
            logging.warning("Error while running the test: ", exc_info=True)

        self.shutdown_tests()

        for client in self.api_clients:
            if client.test_id:
                self.download_artifacts(client, client.test_id, download_list)

        return retcode

    def shutdown_tests(self):
        for client in self.api_clients:
            if client.test_id:
                client.get_test_status(client.test_id)
                if client.test_status not in [LunaparkTank.TEST_FINISHED, LunaparkTank.TEST_NOT_FOUND]:
                    try:
                        client.stop_test()
                    except Exception as exc:
                        logging.error("Error interrupting test at %s: %s", client.host, traceback.format_exc(exc))

        for client in self.api_clients:
            if client.test_id:
                first_pass = True
                while client.test_status not in [LunaparkTank.TEST_FINISHED, LunaparkTank.TEST_NOT_FOUND]:
                    client.get_test_status(client.test_id)
                    if not first_pass:
                        logging.info("Finalizing %s: %s", client.host, client.test_status)
                        time.sleep(self.retry_interval)
                    first_pass = False

    def wait(self, jobno_file):
        while True:
            has_unfinished = False
            for client in self.api_clients:
                if client.test_status == LunaparkTank.TEST_FINISHED:
                    continue

                has_unfinished = True

                res = client.get_test_status(client.test_id)
                if len(self.api_clients) > 1:
                    if res['lunapark_id'] and not res['lunapark_id'].startswith("meta-"):
                        self.save_status(jobno_file, 'meta', res['lunapark_id'])
                else:
                    self.save_status(jobno_file, client.test_id, res['lunapark_id'])

                if not res['success']:
                    raise InterruptedTestException(res['error'], 1)

                if not self.reported_web_link and res['lunapark_id'] and not str(res['lunapark_id']).startswith("meta-"):
                    logging.info("Web link: %s", res['lunapark_url'])
                    self.reported_web_link = True

                tank_msg = res['tank_msg']
                if tank_msg:
                    logging.error("Message from tank: %s", tank_msg)

                if res['left_time']:
                    eta = ', ETA: %s' % datetime.timedelta(seconds=res['left_time'])
                else:
                    eta = ''

                status = res['status_code']
                logging.info("The test at %s is %s%s", client.host, status, eta)

                if res['exit_code']:
                    raise InterruptedTestException("Non-zero exit code at %s: %s" % (client.host, res['exit_code']),
                                                   res['exit_code'])

            if not has_unfinished:
                break
            else:
                time.sleep(self.retry_interval)

    def save_status(self, jobno_file, test_id, jobno):
        if jobno_file:
            logging.debug("Saving test id to %s", jobno_file)
            handle = open(jobno_file, 'w')
            handle.write('%s\n%s\n' % (test_id, jobno))
            handle.close()

    def download_artifacts(self, api, test_id, download_list):
        res = api.get_test_logs(test_id)
        logging.info('Test artifacts at %s: %s', api.host, res)
        if download_list:
            logging.info("Requesting download: %s", download_list)
            for fname in download_list:
                for artifact in res:
                    if fnmatch.fnmatch(artifact, fname):
                        logging.info("Downloading artifact: %s...", artifact)
                        api.get_test_log(test_id, artifact, artifact)


class LunaparkAPIError(Exception):
    """
        Общий тип исключений для Lunapark API
    """
    pass


class IncorrectTestStatus(LunaparkAPIError):
    """
        Танк вернул неправильный статус в ответе
    """
    pass


class LunaparkTank(object):
    """
        Класс обёртки для работы с API Lunapark танка
    """

    GET_TANK_STATUS_URL = '/api/v1/tank/status.json'
    GET_TEST_STATUS_URL = '/api/v1/tests/%s/status.json'
    START_TEST_URL = '/api/v1/tests/start.json'
    STOP_TEST_URL = '/api/v1/tests/stop.json'
    GET_TEST_LOGS_LIST = '/api/v1/tests/%s/logs.json'
    GET_TEST_LOG = '/api/v1/tests/%s/logs/%s'

    CFG_FIELD_NAME = 'load.conf'
    AMMO_FIELD_NAME = 'ammo'

    TEST_PREPARING = "PREPARING"
    TEST_NOT_FOUND = "NOT_FOUND"
    TEST_RUNNING = "RUNNING"
    TEST_FINISHING = "FINISHING"
    TEST_FINISHED = "FINISHED"

    def __init__(self, host='localhost', port=8083, timeout=3):
        self.port = int(port)
        self.host = str(host)
        self.timeout = timeout
        self.test_id = 0
        socket.setdefaulttimeout(self.timeout)
        self.test_status = self.TEST_NOT_FOUND
        self.user_agent = self.get_user_agent()
        self.wait_exponential_multiplier=1000
        self.wait_exponential_max=10000
        self.stop_max_delay=180000

    def __repr__(self):
        return 'Tank {host}:{port}, test {test_id} with status {test_status}'.format(
            host=self.host, port=self.port, test_id=self.test_id, test_status=self.test_status
        )

    @staticmethod
    def get_user_agent():
        tankapi_cmd_agent = 'tankapi-client/{}'.format(VERSION)
        py_info = sys.version_info
        python_agent = 'Python/{}.{}.{}'.format(
            py_info[0], py_info[1], py_info[2])
        os_agent = 'OS/{}'.format(platform.platform())
        return ' '.join((tankapi_cmd_agent, python_agent, os_agent))

    def __get_url_from_path(self, path):
        return 'http://%s:%s%s' % (self.host, self.port, path)

    def call_tank_api_url_raw(self, url):
        return urlopen(
            self.__get_url_from_path(url),
            timeout=self.timeout
        ).read().decode('utf-8')

    def call_tank_api_url(self, url):
        """
            Вызов url из API танка
            @return: текст ответа после вызова url-а
        """
        api_response = json.loads(self.call_tank_api_url_raw(url))
        logging.debug("GET %s: %s", url, api_response)
        return api_response

    def get_status(self):
        """
            Получить информацию о состоянии танка.
            @id: идентификатор
            @return: словарь с информацией о состоянии танка
        """
        @retry(wait_exponential_multiplier=self.wait_exponential_multiplier,
               wait_exponential_max=self.wait_exponential_max,
               stop_max_delay=self.stop_max_delay)
        def inner(*args):
            return self.call_tank_api_url(self.GET_TANK_STATUS_URL)
        return inner(self)

    # Exponential retry in case of any exception for 180 seconds, after that exception is raised
    def get_test_status(self, test_id):
        """
            Получить информацию о состоянии выполнения теста на танке
            @test_id: идентификатор задачи
            @return: словарь с информацией о состоянии теста
        """
        @retry(wait_exponential_multiplier=self.wait_exponential_multiplier,
               wait_exponential_max=self.wait_exponential_max,
               stop_max_delay=self.stop_max_delay,
               retry_on_exception=lambda e: isinstance(e, Exception))
        def inner(*args):
            res = self.call_tank_api_url(self.GET_TEST_STATUS_URL % test_id)
            self.test_status = res['status_code']
            return res
        return inner(self, test_id)

    def post_multipart(self, host, selector, payload_data, files, encoding='utf-8'):
        post_headers = dict()
        post_headers['Charset'] = encoding
        post_headers['User-Agent'] = self.user_agent
        logging.debug("POST payload data: %s", payload_data)
        logging.debug("POST payload files: %s", files)
        logging.debug("POST headers: %s", post_headers)
        try:
            logging.debug("Trying to send ammos/configs to tank")
            res = requests.post("http://"+host+selector, files=files, data=payload_data, headers=post_headers)
            logging.debug("TankAPI response: %s", res.text)
            return res.text
        except Exception as exc:
            logging.error("Seems like there is no connection to tank: %s, url: %s", host, selector)
            logging.debug(exc)
            return json.dumps({'error': repr(exc)})

    def __get_content_type(self, filename):
        """
            get_content_type from http://code.activestate.com/recipes/146306/
        """
        return mimetypes.guess_type(filename)[0] or 'application/octet-stream'

    def start_test(self, cfg_list, ammo, files, cli_options, cfg_patches):
        """
            Запустить стрельбу по заданному конфигу и патронам
            @load_conf: путь до файла конфига
            @ammo: путь до файла с патронами
            @return: словарь с информацией о идентификаторе запущенного теста или об ошибке
        """
        if files is None:
            files = []
        if cli_options is None:
            cli_options = []

        multipart_files = [self.__prepare_text_file(cfg, self.CFG_FIELD_NAME) for cfg in cfg_list] + \
                          ([self.__prepare_text_file(ammo, self.AMMO_FIELD_NAME)] if ammo else []) +\
                          [self.__prepare_text_file(f, os.path.basename(f)) for f in files]

        post_result = self.post_multipart('%s:%s' % (self.host, self.port),
                                          self.START_TEST_URL,
                                          {'options': cli_options,
                                           'cfg_patch': cfg_patches},
                                          multipart_files)
        return json.loads(str(post_result))

    @staticmethod
    def __prepare_text_file(filename, form_field_name):
        path = os.path.abspath(filename)
        if not os.path.exists(path):
            raise LunaparkAPIError('File %s does not exist.' % path)
        if not os.path.isfile(path):
            raise LunaparkAPIError('%s is not a filename.' % path)
        return form_field_name, (os.path.basename(filename), open(path, 'rb'), 'text/plain')

    def __check_test_status(self, status):
        """
            Проверяет значение статуса
            @return: переданное значение статуса, если оно в порядке
            Если значение некорректное, вызывается исключение IncorrectTestStatus
        """
        if status in (self.TEST_FINISHED, self.TEST_PREPARING, self.TEST_NOT_FOUND, self.TEST_RUNNING, self.TEST_FINISHING):
            return status
        else:
            raise IncorrectTestStatus('Incorrect status %s' % status)

    def test_exists(self, test_id):
        """
            Проверяет, существует ли тест
            @test_id: идентификатор теста
            @returns: True, если тест существует, False в противном случае
        """
        test_status = self.__check_test_status(self.get_test_status(test_id)['status_code'])
        if test_status['status_code'] == self.TEST_NOT_FOUND:
            return False
        else:
            return True

    def get_test_logs(self, test_id):
        """
            Получить список файлов логов для теста @test_id
            Если таск не найден или произошла другая ошибка, вызвается исключение LunaparkAPIError
            @test_id: идентификатор теста
            @return: список файлов логов
        """
        result = self.call_tank_api_url(self.GET_TEST_LOGS_LIST % test_id)
        if result['error']:
            raise LunaparkAPIError('Cannot get log files for task %s, error: %s' % (test_id, result['error']))
        return result['files']

    def stop_test(self):
        """
            Остановить тест
            @test_id: идентификатор теста
            @return: True, если тест был остановлен, в противном случае выбрасывается исключение
        """
        logging.info("Stopping test at %s", self.host)
        result = self.call_tank_api_url(self.STOP_TEST_URL)
        if result['error']:
            raise LunaparkAPIError('Cannot stop test, error: %s' % result['error'])
        return True

    def get_test_log(self, test_id, log_name, save_to_file):
        result = self.call_tank_api_url_raw(self.GET_TEST_LOG % (test_id, log_name))
        fhandle = open(save_to_file, 'wb')
        fhandle.write(result)
        fhandle.close()


class InterruptedTestException(Exception):
    def __init__(self, message, retcode):
        Exception.__init__(self, message)
        self.retcode = retcode
