import re
import yaml
from copy import deepcopy
from enum import Enum as PyEnum

from load.projects.cloud.loadtesting.config import EnvType, ENV_CONFIG
from yandex.cloud.priv.loadtesting.v1 import tank_job_pb2 as messages
from yandextank.validator.validator import TankConfig, ValidationError
from load.projects.cloud.loadtesting.db.tables import JobConfigTable
from load.projects.cloud.loadtesting.server.api.common.utils import generate_cloud_id

MONITORING_PORT = 1234

PANDORA_BASE_CONFIG = {
    'enabled': True,
    'package': 'yandextank.plugins.Pandora',
    'config_content': {
        'pools': [
            {
                'id': 'HTTP',
                'gun': {
                    'type': 'http',
                },
                'ammo': {},
                'result': {
                    'type': 'phout',
                    'destination': './phout.log',
                },
                'startup': {
                    'type': 'once',
                }
            }
        ],
        'log': {'level': 'error'},
        'monitoring': {'expvar': {'enabled': True, 'port': MONITORING_PORT}}
    }
}


class Autostop(PyEnum):
    AUTOSTOP_UNSPECIFIED = 'AUTOSTOP_UNSPECIFIED'
    TIME = 'TIME'
    HTTP = 'HTTP'
    NET = 'NET'
    QUANTILE = 'QUANTILE'
    INSTANCES = 'INSTANCES'
    METRIC_LOWER = 'METRIC_LOWER'
    METRIC_HIGHER = 'METRIC_HIGHER'
    STEADY_CUMULATIVE = 'STEADY_CUMULATIVE'
    LIMIT = 'LIMIT'
    TOTAL_TIME = 'TOTAL_TIME'
    TOTAL_HTTP = 'TOTAL_HTTP'
    TOTAL_NET = 'TOTAL_NET'
    NEGATIVE_HTTP = 'NEGATIVE_HTTP'
    NEGATIVE_NET = 'NEGATIVE_NET'
    HTTP_TREND = 'HTTP_TREND'


class JobConfig(JobConfigTable):

    def __init__(self):
        self.id = generate_cloud_id()
        self.error: str = ''
        self.content: str = ''
        self.uploader: str = None

    def make_from_scratch(self, db, request, agent_version_revision=0):
        """
        Create config with field values.

        NB: Now there are two ways to pass config to tank from UI:
         - fill in the fields in the form, and config will be generated on backend,
           OR
         - upload yaml file or fill in the field "YAML config", it will be taken as is.
        At the moment we do not support merging the fields values with yaml file, though Tank supports it.
        """

        # FIXME delete, when there are no agents with old versions CLOUDLOAD-172
        if agent_version_revision >= 9171788:
            plugin = 'yandextank.plugins.DataUploader'
            self.uploader = 'uploader'
        else:
            plugin = 'yandextank.plugins.CloudUploader'
            self.uploader = 'cloudloader'

        content = {
            self.uploader: {
                'enabled': True,
                'package': plugin,
                'job_name': request.name,
                'job_dsc': request.description,
                'ver': request.target_version,
                'api_address': ENV_CONFIG.SERVER_URL
            }
        }

        ammo = None
        if request.ammo_id:
            ammo = db.ammo.get(request.ammo_id)
        elif request.HasField('test_data'):
            ammo = db.ammo.get_by_name(request.test_data.object_storage_filename, request.test_data.object_storage_bucket, request.folder_id)
        if request.load_schedule and request.load_schedule.load_type:
            load_type = messages.LoadType.Name(request.load_schedule.load_type).lower()
        else:
            schedule = ''
            load_type = ''
        if request.ammo_type:
            ammo_type = self._rewrite_ammo_type(
                messages.AmmoType.Name(request.ammo_type).lower()
            )
        else:
            ammo_type = ''
        if request.autostops:
            content.update({'autostop': {
                'enabled': True,
                'package': 'yandextank.plugins.Autostop',
                'autostop': []
            }})

            for item in request.autostops:
                content['autostop']['autostop'].append(
                    f'{messages.Autostop.AutostopType.Name(item.autostop_type).lower()}({item.autostop_criteria})'
                )

        # https://st.yandex-team.ru/CLOUDLOAD-17#61151d9a5a3e20776a19ee0e
        if request.generator == messages.TankJob.Generator.PANDORA:
            if not ammo_type:
                self.error = 'No ammo_type specified'
                return

            schedule = []
            if request.load_schedule and request.load_schedule.load_profile:
                for profile in request.load_schedule.load_profile:
                    try:
                        profile = re.sub(r':(\S)', r': \1', profile)
                        schedule_line = yaml.safe_load(profile)
                        assert isinstance(schedule_line, dict)
                        schedule.append(schedule_line)
                    except (AssertionError, yaml.YAMLError):
                        self.error = f'Load profile {profile} is not valid'

            content['pandora'] = deepcopy(PANDORA_BASE_CONFIG)
            for pool in content['pandora']['config_content']['pools']:
                pool['gun']['target'] = f'{request.target_address}:{request.target_port}'
                pool['ammo']['type'] = ammo_type
                if request.ammo_urls:
                    pool['ammo']['uris'] = [url for url in request.ammo_urls]
                if request.ammo_headers:
                    pool['ammo']['headers'] = [header for header in request.ammo_headers]
                if ammo is not None:
                    pool['ammo']['file'] = ammo.s3_name
                pool['rps'] = schedule
                pool['gun']['ssl'] = request.ssl
                if request.instances:
                    pool['startup']['times'] = request.instances
                else:
                    pool['startup']['times'] = 1000

            self.content = content

        elif request.generator == messages.TankJob.Generator.PHANTOM:

            if not ammo_type:
                ammo_type = 'phantom'
            if request.load_schedule and request.load_schedule.load_profile:
                schedule = ' '.join([profile for profile in request.load_schedule.load_profile])
            content['phantom'] = {
                'enabled': True,
                'package': 'yandextank.plugins.Phantom',
                'address': f'{request.target_address}:{request.target_port}',
                'ammo_type': ammo_type,
                'load_profile': {'load_type': load_type, 'schedule': schedule},
                'ssl': request.ssl
            }
            if request.instances:
                content['phantom']['instances'] = request.instances
            if ammo is not None:
                content['phantom']['ammofile'] = ammo.s3_name
            if request.ammo_urls:
                content['phantom']['uris'] = [url for url in request.ammo_urls]
            if request.ammo_headers:
                content['phantom']['headers'] = [header for header in request.ammo_headers]

            self.content = content

        else:
            self.error = 'Unknown generator type'

    @staticmethod
    def _rewrite_ammo_type(ammo_type=None):
        if ammo_type == 'http_json':
            ammo_type = 'http/json'
        return ammo_type

    def rewrite_ammo(self, db, request):
        ammo = None
        if request.ammo_id:
            ammo = db.ammo.get(request.ammo_id)
        elif request.HasField('test_data'):
            ammo = db.ammo.get_by_name(request.test_data.object_storage_filename, request.test_data.object_storage_bucket, request.folder_id)

        if ammo is not None:
            if self.content.get('pandora', {}).get('enabled'):
                self.content['pandora']['config_content']['pools'][0]['ammo'].update({'file': ammo.s3_name})
            elif self.content.get('phantom', {}).get('enabled', False):
                self.content['phantom']['ammofile'] = ammo.s3_name
                self.content['phantom']['uris'] = []

    def add_generator_debug(self):
        if ENV_CONFIG.ENV_TYPE == EnvType.PREPROD.value:
            self.content.setdefault('core', {})
            self.content['core'].update({'debug': True})

    def deserialize(self):
        try:
            content = yaml.safe_load(self.raw_content)
            assert isinstance(content, dict)
            self.content = content
            if self.content.get('cloudloader', {}).get('enabled') is True:
                self.uploader = 'cloudloader'
            elif self.content.get('uploader', {}).get('enabled') is True:
                self.uploader = 'uploader'
            else:
                self.error = 'Cloudloader or uploader section is missing'
        except AssertionError:
            self.error = 'Config is not valid yaml'
        except Exception as exc:
            self.error = f'{exc}'

    def validate(self):
        """
        Validate config with yandex tank validator and set validation_status
        """
        config_content = self.content
        cloudloader_section = config_content.pop('cloudloader') if self.uploader == 'cloudloader' else {}
        try:
            _, content = TankConfig([config_content], with_dynamic_options=False).validate()
        except ValidationError as e:
            self.error = str(e)
            return

        if cloudloader_section:
            content['cloudloader'] = cloudloader_section
        self.content = content

    @staticmethod
    def check_sanity():
        """
        There should be some sanity checks for test: too heavy ammo, too long schedule, etc.
        :return: True if config is considered to be sane, False otherwise
        """
        return True
