import re
import time
import yaml
import logging
import datetime
from clickhouse_driver import Client

from sqlalchemy import Column, DateTime, Integer, String, ForeignKey, Float, text, Text, BigInteger, SmallInteger, desc
from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound
from sqlalchemy.orm import relationship
from flask_sqlalchemy import SQLAlchemy

from .query import RetryingQuery
from .loadscheme_utils import LoadSchemeIterator, Ini2Json
from load.projects.lunaparkapi.settings import base

db = SQLAlchemy(query_class=RetryingQuery)
ch_client = Client(
    host=base['CLICKHOUSE_HOST'],
    port=base['CLICKHOUSE_PORT'],
    database=base['CLICKHOUSE_DATABASE'],
    user=base['CLICKHOUSE_USER'],
    password=base['CLICKHOUSE_PASSWORD'],
    secure=True,
    verify=False,
    settings={'joined_subquery_requires_alias': 0}
)


def xss_escape(s):
    return s.replace('>', '&gt;').replace('<', '&lt;')


def escape_string(s):
    return s.replace('\\', '\\\\').replace("'", "\\'")


# noinspection PyBroadException
class Job(db.Model):
    __tablename__ = 'job'

    n = Column(Integer, primary_key=True, index=True, server_default=text("nextval('job_n_seq'::regclass)"))
    name = Column(String(120))
    dsc = Column(Text)
    fd = Column(DateTime, nullable=False, server_default=text("now()"))
    td = Column(DateTime)
    person = Column(String(64))
    # TODO: why task is not a ForeignKey?
    task = Column(String(120))
    ammo_path = Column(String(256))
    ver = Column(String(120))
    instances = Column(BigInteger, nullable=False)
    loop_cnt = Column(Float)
    configinitial = Column(Text)
    configinfo = Column(Text)
    quit_status = Column(SmallInteger)
    tank = Column(ForeignKey('server.n'), index=True)
    component = Column(ForeignKey('component.n'), nullable=False, index=True)
    srv = Column(ForeignKey('server.n'), index=True)
    srv_port = Column(BigInteger, nullable=False)

    component1 = relationship('Component')
    server = relationship('Server', primaryjoin='Job.srv == Server.n')
    server1 = relationship('Server', primaryjoin='Job.tank == Server.n')

    @property
    def id(self):
        """
        n alias
        :return: int
        """
        return int(self.n)

    @property
    def date(self):
        """start alias"""
        return self.start

    @property
    def start(self):
        return int(time.mktime(self.fd.timetuple()))

    @property
    def end(self):
        return int(time.mktime(self.td.timetuple())) if self.td else None

    @property
    def is_online(self):
        return not bool(self.td)

    # TODO: caching solution LUNAPARK-3641
    @property
    def cases(self):
        """
        returns a sorted list of cases
        """
        try:
            sql = """
                select distinct tag from loaddb.rt_microsecond_details_buffer
                    where job_id={job}
                    order by tag
                    """.format(job=self.id)
            cases = [xss_escape(case[0]) for case in ch_client.execute(sql)]
            cases.append('')
        except Exception:
            logging.warning("Could not get job {} cases due to: ".format(self.n), exc_info=True)
            cases = []
        return cases

    # TODO: caching solution LUNAPARK-3641
    @property
    def scheme_type(self):
        nonzero_reqps = ch_client.execute(
            '''
            select any(job_id)
                from loaddb.rt_microsecond_details_buffer
                where job_id={job}
                    and job_date=toDate({job_date})
                    and reqps!=0
            '''.format(job=self.id, job_date=self.date)
        )
        if nonzero_reqps:
            return 'rps'

        nonzero_threads = ch_client.execute(
            '''
            select any(job_id)
                from loaddb.rt_microsecond_details_buffer
                where job_id={job}
                    and job_date=toDate({job_date})
                    and threads!=0
            '''.format(job=self.id, job_date=self.date)
        )
        if nonzero_threads:
            return 'instances'

        # Для случаев ручной заливки пхаута (?)
        return 'rps'

    @property
    def config(self):
        config_str = self.configinitial or self.configinfo

        try:
            config = yaml.safe_load(config_str.replace('!!python/unicode ', ''))
            assert isinstance(config, dict), 'Invalid config {}'.format(config_str)
            return config
        except (yaml.parser.ParserError, AssertionError):
            logging.info('Failed to parse config. Trying ini conversion...')
            try:
                return Ini2Json(config_str).convert()
            except Exception:
                logging.warning('Failed to parse config', exc_info=True)
            return {}

    @property
    def quit_status_text(self):
        """
        converting quit_status codes into text
        """
        exit_status = {
            '0': 'completed',
            '1': 'interrupted_generic_interrupt',
            '2': 'interrupted',
            '3': 'interrupted_active_task_not_found ',
            '4': 'interrupted_no_ammo_file',
            '5': 'interrupted_address_not_specified',
            '6': 'interrupted_cpu_or_disk_overload',
            '7': 'interrupted_unknown_config_parameter',
            '8': 'interrupted_stop_via_web',
            '9': 'interrupted',
            '11': 'interrupted_job_number_error',
            '12': 'interrupted_phantom_error',
            '13': 'interrupted_job_metainfo_error',
            '14': 'interrupted_target_monitoring_error',
            '15': 'interrupted_target_info_error',
            '21': 'autostop_time',
            '22': 'autostop_http',
            '23': 'autostop_net',
            '24': 'autostop_instances',
            '25': 'autostop_total_time',
            '26': 'autostop_total_http',
            '27': 'autostop_total_net',
            '28': 'autostop_negative_http',
            '29': 'autostop_negative_net',
            '30': 'autostop_http_trend',
            '31': 'autostop_metric_higher',
            '32': 'autostop_metric_lower',
            '100': 'killed_as_stale',
            '': 'online'
        }
        try:
            exit_code = str(self.quit_status)
            if exit_code in exit_status:
                return exit_status[exit_code]
            else:
                return 'other'
        except Exception:
            logging.error('Could not get quit_status for job {}'.format(self.n), exc_info=True)
            return 'other'

    @property
    def data_started(self):
        """
        for online jobs returns first trail or job.fd
        for offline jobs returns JobTrail.trail_start or first Trail.time or job.fd
        :return unix timestamp
        """
        try:
            job_trail = JobTrail.query.filter_by(up=self.id).one()
            return job_trail.trail_start
        except (NoResultFound, MultipleResultsFound):
            logging.debug('No JobTrail or multiple JobTrails for offline job %s; '
                          'define data_started by clickhouse data', self.n)
        try:
            query_first_trail = '''select min(time)
                from loaddb.rt_microsecond_details_buffer
                where job_id={job}
                and job_date=toDate({job_date})
                and time!='0000-00-00 00:00:00'
                '''.format(job=self.id, job_date=self.date)
            first_trail_time = ch_client.execute(query_first_trail)[0][0]
            if first_trail_time == '0000-00-00 00:00:00':
                first_trail_time = '0001-01-01 00:00:00'
            return int(time.mktime(datetime.datetime.timetuple(first_trail_time)))
        except IndexError:
            logging.debug('No first trail for offline job %s,take job.fd as data_started', self.n)
            return self.fd
        except Exception:
            logging.debug('Could not get Job %s data_started due to ', self.n, exc_info=True)
            return self.fd

    @property
    def data_stopped(self):
        """
        for online jobs returns None
        for offline jobs returns JobTrail.trail_stop or last Trail.time or job.td
        :return unix timestamp
        """
        try:
            assert self.td
            job_trail = JobTrail.query.filter_by(up=self.id).one()
            return job_trail.trail_stop
        except AssertionError:
            logging.debug('No data_stopped for online job {}'.format(self.n))
            return None
        except (MultipleResultsFound, NoResultFound):
            logging.debug('No JobTrail or multiple JobTrails for offline job %s. '
                          'Define data_stopped by clickhouse data', self.n)
        try:
            query_last_trail = '''select max(time)
                                from loaddb.rt_microsecond_details_buffer
                                where job_id={job}
                                and job_date=toDate({job_date})
                                and time!='0000-00-00 00:00:00'
                                '''.format(job=self.id, job_date=self.date)
            last_trail_time = ch_client.execute(query_last_trail)[0][0]
            if last_trail_time == '0000-00-00 00:00:00':
                last_trail_time = '0001-01-01 00:00:00'
            return int(time.mktime(datetime.datetime.timetuple(last_trail_time)))
        except IndexError:
            logging.debug('No Trail for offline job %s, get job.td as data_stopped', self.n)
            return self.td
        except Exception:
            logging.debug('Could not get Job %s data_stopped due to:', self.n, exc_info=True)
            return self.td

    @property
    def duration(self):
        """
        self.data_stopped - self.data_started for finished jobs
        :rtype int
        """
        if self.td:
            duration = self.data_stopped - self.data_started
            if isinstance(duration, int):
                return duration + 1
            else:
                return duration.seconds + 1
        try:
            query_last_trail = '''
                select toInt32(max(time))
                from loaddb.rt_microsecond_details_buffer
                where job_id={job}
                and job_date=toDate({job_date})
            '''.format(job=self.id, job_date=self.date)
            last_trail_time = ch_client.execute(query_last_trail)
            last_trail_time = last_trail_time[0][0] if last_trail_time else int(time.time())
            return last_trail_time - int(time.mktime(datetime.datetime.timetuple(self.data_started)))
        except Exception:
            logging.warning('Could not get online Job {} duration due to:'.format(self.n), exc_info=True)
            return 0

    @property
    def imbalance(self):
        try:
            imbalance = JobImbalance.query.filter_by(up=self.id).order_by(desc(JobImbalance.n)).first()
            if imbalance:
                if imbalance.hum_processed:
                    return imbalance.hum_imbalance
                else:
                    return imbalance.rob_imbalance or None
            else:
                return None
        except (MultipleResultsFound, NoResultFound):
            return None

    @property
    def loadschemes(self):
        """
        List of loadscheme objects (or pseudo loadscheme objects if retrieved from configinfo)
        """
        try:
            job_duration = self.data_stopped - self.data_started
            if not isinstance(job_duration, int):
                job_duration = (self.data_stopped - self.data_started).seconds
            schemes = Loadscheme.query.filter(
                Loadscheme.up == self.n, Loadscheme.sec_from <= job_duration
            ).order_by('sec_from')
            loadschemes = [ls for ls in schemes]
            assert loadschemes
            return loadschemes
        except AssertionError:
            logging.debug('No loadschemes found in db. Trying to extract them from config...')
        except Exception:
            logging.error('Failed on getting loadscheme from database for job {}'.format(self.n), exc_info=True)
        try:
            # TODO: pandora loadscheme
            configinfo_json = self.config
            phantom_section = configinfo_json.get('phantom', {})
            bfg_section = configinfo_json.get('bfg', {})
            schedule = phantom_section.get('rps_schedule', '') \
                or bfg_section.get('rps_schedule', '') \
                or phantom_section.get('instances_schedule', '') \
                or bfg_section.get('instances_schedule', '')
            schedule = schedule.strip()
            if re.match(r'%\([a-zA-Z_]+\)s', schedule):
                schedule = configinfo_json.get('DEFAULT', {}) \
                    .get(schedule[schedule.index('(') + 1:schedule.index(')')], '')
            if schedule.strip():
                schemes = LoadSchemeIterator(
                    (sched for sched in schedule.replace(' ', '').replace(')', ') ').split(' ') if sched),
                    self.n
                )
                return [PseudoLoadscheme(**ls) for ls in schemes]
            else:
                logging.warning('No loadscheme for job {}'.format(self.n))
                return []
        except Exception:
            logging.error('Failed to get loadschemes for job {}'.format(self.id), exc_info=True)
            return []


class JobTrail(db.Model):
    __tablename__ = 'job_trail'

    n = Column(Integer, primary_key=True, server_default=text("nextval('job_trail_n_seq'::regclass)"))
    up = Column(ForeignKey('job.n'), nullable=False, index=True)
    min_rps = Column(Integer)
    max_rps = Column(Integer)
    http = Column(String(64), nullable=False)
    net = Column(Integer, nullable=False)
    trail_start = Column(DateTime)
    trail_stop = Column(DateTime)
    avg_expect = Column(Float(53))
    avg_connect_time = Column(Float(53))
    avg_send_time = Column(Float(53))
    avg_latency = Column(Float(53))
    avg_receive_time = Column(Float(53))
    q50 = Column(Float(53))
    q75 = Column(Float(53))
    q80 = Column(Float(53))
    q85 = Column(Float(53))
    q90 = Column(Float(53))
    q95 = Column(Float(53))
    q98 = Column(Float(53))
    q99 = Column(Float(53))
    avg_resps = Column(Float(53))

    job = relationship('Job')


class Task(db.Model):
    __tablename__ = 'task'
    n = Column(Integer, primary_key=True, server_default=text("nextval('task_n_seq'::regclass)"))
    key = Column(String(64))


class JobImbalance(db.Model):
    __tablename__ = 'job_imbalance'
    n = Column(Integer, primary_key=True, server_default=text("nextval('job_imbalance_n_seq'::regclass)"))
    up = Column(ForeignKey('job.n'), nullable=False, index=True)
    hum_isimbalance = Column(Integer, nullable=False)
    hum_imbalance = Column(Integer, nullable=False)
    rob_isimbalance = Column(Integer, nullable=False)
    rob_warning_sec = Column(Integer, nullable=False)
    rob_imbalance = Column(Integer, nullable=False)
    rob_imbalance_sec = Column(Integer, nullable=False)
    hum_processed = Column(Integer, nullable=False)
    user = Column(String(64))
    job = relationship('Job')


class Component(db.Model):
    __tablename__ = 'component'

    n = Column(Integer, primary_key=True, server_default=text("nextval('component_n_seq'::regclass)"))
    name = Column(String(128), nullable=False)
    dsc = Column(Text)
    priority = Column(Integer, nullable=False)
    include_qs = Column(String(128), nullable=False)
    exclude_qs = Column(String(128), nullable=False)
    job_order = Column(String(32), nullable=False)
    tag = Column(String(128))
    services_json = Column(Text)


class Server(db.Model):
    __tablename__ = 'server'

    n = Column(Integer, primary_key=True, server_default=text("nextval('server_n_seq'::regclass)"))
    host = Column(String(128), nullable=False)
    is_test = Column(SmallInteger)
    fd = Column(DateTime, nullable=False, server_default=text('now()'))
    td = Column(DateTime)
    config = Column(Text)
    dsc = Column(Text)
    last_ip = Column(String(20))
    last_dc = Column(Integer)


class Loadscheme(db.Model):
    __tablename__ = 'loadscheme'

    n = Column(Integer, primary_key=True, server_default=text("nextval('loadscheme_n_seq'::regclass)"))
    up = Column(Integer, nullable=False)
    sec_from = Column(BigInteger, nullable=False)
    sec_to = Column(BigInteger, nullable=False)
    load_type = Column(BigInteger, nullable=False)
    load_from = Column(Float, nullable=False)
    load_to = Column(Float, nullable=False)
    dsc = Column(String(128), nullable=False)


class PseudoLoadscheme:
    """
    Fake loadcheme for scheme taken from configinfo
    """

    def __init__(self, up, load_type, sec_from, sec_to, load_from, load_to, dsc):
        self.up = up
        self.load_type = load_type
        self.sec_from = sec_from
        self.sec_to = sec_to
        self.load_from = load_from
        self.load_to = load_to
        self.dsc = dsc
