import logging
import re

from flask import request
from clickhouse_driver import Client
from sqlalchemy import desc, func
from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound

from sandbox.yasandbox import context
from sandbox.serviceapi.web import exceptions
from load.projects.lunaparkapi.settings import base
from load.projects.lunaparkapi.database.models import Job, Task, Server, Component
from load.projects.lunaparkapi.handlers.report_data.report_data import get_quantiles_data, get_instances_data, get_proto_codes_data, get_net_codes_data
from .aggregator import ProtoCodesAggregator, NetCodesAggregator, CumulativeQuantilesAggregator

clickhouse_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 __validate_job_id(job_id):
    """
    Validate whether given job_id is found in db
    :param job_id: Test id
    :type job_id: int
    :return: Job object
    :raise
      sandbox.serviceapi.web.exceptions.BadRequest - if job id is not given or is not int
      sandbox.serviceapi.web.exceptions.NotFound - if job is not found by this job id
    """

    if job_id:
        try:
            job_n = int(job_id)
            job = Job.query.filter_by(n=job_n).one()
            return job
        except ValueError:
            raise exceptions.BadRequest('Job id {} not valid'.format(job_id))
        except NoResultFound:
            raise exceptions.NotFound('Job id {} is not found'.format(job_id))
        except MultipleResultsFound:
            raise exceptions.ServiceUnavailable('Postgres error: several jobs found by id {}'.format(job_id))
    else:
        raise exceptions.BadRequest('Job id must be given as argument')


def __validate_task_id(task_name):
    """
    Validate whether given task_id is found in db
    :param task_name: Task name, must match regexp for ticket name
    :type task_name: str
    :return: Task object
    :raise
      sandbox.serviceapi.web.exceptions.BadRequest - if task name is not given or does not match ticket regexp
      sandbox.serviceapi.web.exceptions.NotFound - if task is not found by this task_name
    """
    if task_name is not None:
        try:
            assert re.match(r'[a-zA-Z]+-\d+', task_name)
            task_name = str(task_name).upper()
            task = Task.query.filter(Task.key == task_name).one()
            return task
        except AssertionError:
            raise exceptions.BadRequest('Task name {} is not valid ticket name'.format(task_name))
        except NoResultFound:
            raise exceptions.NotFound('Task {} is not found'.format(task_name))
        except MultipleResultsFound:
            raise exceptions.ServiceUnavailable('Postgres error: several tickets found by name {}'.format(task_name))
    else:
        raise exceptions.BadRequest('Task name must be given as argument')


def get_report():
    chart_type = request.args.get('chart', None)
    job_id = request.args.get('job', None)
    task_name = request.args.get('task', None)

    # charts = {
    #     'quantiles': get_quantiles_data,
    #     'proto_codes': get_proto_codes_data,
    #     'net_codes': get_net_codes_data,
    #     'instances': get_instance_data,
    #     'table_proto_codes': get_table_proto,
    #     'table_net_codes': get_table_net,
    #     'table_cum_quantiles': get_table_quantiles,
    #     'cases': get_cases,
    #     'jobs': get_jobs_for_task,
    # }

    if not chart_type:
        raise exceptions.BadRequest('Sorry, you have forgotten to define chart type')
    elif chart_type == 'quantiles':
        job = __validate_job_id(job_id)
        return get_quantiles(job)
    elif chart_type == 'proto_codes':
        job = __validate_job_id(job_id)
        return get_proto_codes(job)
    elif chart_type == 'net_codes':
        job = __validate_job_id(job_id)
        return get_net_codes(job)
    elif chart_type == 'instances':
        job = __validate_job_id(job_id)
        return get_instances(job)
    elif chart_type == 'table_proto_codes':
        job = __validate_job_id(job_id)
        return get_table_proto(job)
    elif chart_type == 'table_net_codes':
        job = __validate_job_id(job_id)
        return get_table_net(job)
    elif chart_type == 'table_cum_quantiles':
        job = __validate_job_id(job_id)
        return get_table_quantiles(job)
    elif chart_type == 'cases':
        job = __validate_job_id(job_id)
        return get_cases(job)
    elif chart_type == 'jobs':
        task = __validate_task_id(task_name)
        return get_jobs_for_task(task)
    elif chart_type == 'current_user':
        # TODO: switch to GET User
        return get_current_user(job_id)
    elif chart_type == 'table_jobs':
        return get_table_jobs()
    elif chart_type == 'table_tasks':
        return get_table_tasks()
    elif chart_type == 'table_ammo':
        return get_table_ammo()
    elif chart_type == 'table_scenarios':
        return get_table_scenarios()
    elif chart_type == 'job_meta':
        job = __validate_job_id(job_id)
        return get_job_metadata(job)
    raise exceptions.BadRequest('Unknown chart type')


# Charts

def get_instances(job):
    result = get_job_metadata(job)
    instance_data = get_instances_data(
        client=clickhouse_client,
        db_name=base['CLICKHOUSE_DATABASE'],
        job_id=job.id, job_date=job.date,
        job_start=job.start, job_end=job.end,
        job_scheme_type=job.scheme_type
    )
    result.update(instance_data)
    result['success'] = 'success'
    return result


def get_quantiles(job):
    """
    Get quantiles data from clickhouse
    :param job: Job object
    :return: quantiles data in json
    """
    result = get_job_metadata(job)
    quantiles_data = get_quantiles_data(
        client=clickhouse_client,
        db_name=base['CLICKHOUSE_DATABASE'],
        job_id=job.id, job_date=job.date,
        job_start=job.start, job_end=job.end,
        job_cases=job.cases,
        job_scheme_type=job.scheme_type
    )
    result.update(quantiles_data)
    result['success'] = 'success'
    return result


def get_net_codes(job):
    """
    Get proto codes data for given job from Clickhouse
    :param job: Job object
    :return: net codes data in dict
    """
    result = get_job_metadata(job)
    net_codes_data = get_net_codes_data(
        client=clickhouse_client, db_name=base['CLICKHOUSE_DATABASE'],
        job_id=job.id, job_date=job.date, job_start=job.start, job_end=job.end,
        job_cases=job.cases, job_scheme_type=job.scheme_type
    )
    result.update(net_codes_data)
    result['success'] = 'success'
    return result


def get_proto_codes(job):
    """
    Get proto codes data for given job from Clickhouse
    :param job: Job object
    :return: proto codes data in dict
    """
    result = get_job_metadata(job)
    proto_codes_data = get_proto_codes_data(
        client=clickhouse_client, db_name=base['CLICKHOUSE_DATABASE'],
        job_id=job.id, job_date=job.date, job_start=job.start, job_end=job.end,
        job_cases=job.cases, job_scheme_type=job.scheme_type
    )
    result.update(proto_codes_data)
    result['success'] = 'success'
    return result


# Tables


def get_table_proto(job):
    aggregator = ProtoCodesAggregator(job_obj=job)
    data = aggregator.aggregate()
    result = get_job_metadata(job)
    result['result'] = 'success'
    result['data'] = data

    return result


def get_table_net(job):
    aggregator = NetCodesAggregator(job_obj=job)
    data = aggregator.aggregate()
    result = get_job_metadata(job)
    result['result'] = 'success'
    result['data'] = data

    return result


def get_table_quantiles(job):
    aggregator = CumulativeQuantilesAggregator(job_obj=job)
    data = aggregator.aggregate()

    result = get_job_metadata(job)
    result['result'] = 'success'
    result['data'] = data

    return result


def get_table_jobs(user=None):
    logging.error('Not implemented: get_table_jobs')
    return {}


def get_table_ammo(user=None):
    logging.error('Not implemented: get_table_ammo')
    return {}


def get_table_scenarios(user=None):
    logging.error('Not implemented: get_table_scenarios')
    return {}


def get_table_tasks():
    try:
        person = context.current.user.staff_info.login
    except AttributeError:
        person = ''

    jobs = Job.query.with_entities(Job.task, func.count(Job.task)).filter(Job.person == person).group_by(Job.task).all()
    tasks = [{'task': task, 'jobs_amount': cnt} for task, cnt in jobs]
    return {'tasks': tasks, 'user': person}


# Additional handlers

def get_cases(job):
    cases = [case for case in job.cases if case]
    cases.append('overall')

    return {'job': job.n, 'cases': cases}


def get_current_user(job_id=None):
    current_user = {
        'login': None,
        'uid': None,
        'is_robot': None,
        'is_dismissed': None,
        'author': None
    }
    if job_id:
        job = __validate_job_id(job_id)
        current_user['author'] = job.person
    if context.current.user:
        current_user.update(
            {
                'login': context.current.user.staff_info.login,
                'uid': context.current.user.staff_info.uid,
                'is_robot': context.current.user.staff_info.is_robot,
                'is_dismissed': context.current.user.staff_info.is_dismissed
            }
        )
    return current_user


def get_jobs_for_task(task):
    # TODO: add offset and limit for collections
    limit = 5000
    task_jobs = Job.query.filter_by(task=str(task.key)).order_by(desc(Job.n)).limit(limit)
    jobs = [{'job': job.n, 'name': job.name, 'date': job.start} for job in task_jobs]
    return {'jobs': jobs, 'task': task.key}


def get_job_metadata(job):
    try:
        job_target = Server.query.filter_by(n=job.srv).one()
        target = '{host}:{port}'.format(host=job_target.host, port=job.srv_port)
    except NoResultFound:
        target = ''

    try:
        job_tank = Server.query.filter_by(n=job.tank).one()
        tank = job_tank.host
    except NoResultFound:
        tank = ''

    try:
        component = Component.query.filter_by(n=job.component).one()
    except NoResultFound:
        component = None

    schemes = []
    prev_dsc = ''
    loadschemes = sorted(job.loadschemes, key=lambda s: s.load_from)
    for ls in loadschemes:
        if not prev_dsc or prev_dsc != ls.dsc:
            schemes.append(ls.dsc)
        prev_dsc = ls.dsc
    load_schedule = ' '.join(schemes) if schemes else ''

    return {
        'job': job.n,
        'meta': {
            'name': job.name,
            'dsc': job.dsc,
            'tank': tank,
            'target': target,
            'start': job.start,
            'end': job.end,
            'duration': job.duration,
            'imbalance': job.imbalance,
            'quit_status_code': job.quit_status,
            'quit_status_text': job.quit_status_text,
            'ammo_path': job.ammo_path,
            'loop_cnt': job.loop_cnt,
            'load_schedule': load_schedule,
            'ver': job.ver,
            'component_name': component.name if component else None,
            'component_code': component.n if component else None,
            'configinitial': job.configinitial or job.configinfo,
            'monitoring': '',
            'task': job.task,
            'artefacts': {}
        }
    }


# Internal functions
