import datetime
import random
import re
from string import digits

import flask
import yaml
from wtforms.validators import DataRequired

from lp_queue import app, db
from lp_queue.models import Task, Status
from lp_queue.util.util import DCFinder, DBCommunicator, claim_task, Formatter, DCs
from flask import Response, request, render_template, flash, redirect, url_for, abort
from flask_wtf import FlaskForm
from wtforms import TextAreaField, SubmitField
import requests
from copy import deepcopy
from yaml import YAMLError
from collections import defaultdict

database = DBCommunicator(db.session)


@app.route('/')
def landing():
    form = EnqueueForm()
    return render_template('landing.html', tasks=Task.query.filter_by(claimed_by=None), form=form)


class EnqueueForm(FlaskForm):
    config = TextAreaField('Task Config', validators=[DataRequired()])
    submit = SubmitField('Enqueue')


@app.route('/enqueue', methods=['POST'])
def enqueue():
    form = EnqueueForm()
    if form.validate_on_submit():
        config = yaml.load(form.config.data)
        result = EnqueueHandler(config).handle()
        flash(yaml.dump(result.data))
        return redirect(url_for('landing'))


@app.route('/job/<int:jobid>/stdout', methods=['GET'])
def get_job_stdout(jobid: int):
    query = "select ts, value from bdk.logs_buffer where job_id='{}' FORMAT TSV".format(jobid)
    r = requests.get(app.config['CC_URL'], params={'query': query})
    return Response('\n'.join([(lambda ts, msg:
                                '{}: {}'.format(datetime.datetime.fromtimestamp(int(ts)/1e3), msg))(*line.split('\t',1))
                               for line in r.text.split('\n') if line]),
                    content_type='text/plain; charset=utf-8')


@app.route('/job/<int:task_id>', methods=['GET'])
def get_task_info(task_id: int):
    task = Task.query.get(task_id)
    if task is not None:
        return flask.jsonify({
            'author': task.author,
            'enqueued_at': task.enqueued_at,
            'claimed_at': task.claimed_at,
            'finished_at': task.finished_at,
            'returned_code': task.return_code,
            'status': str(task.status),
            'config': task.config,
            'stdout link': url_for('get_job_stdout', jobid=task_id)
        })
    else:
        return abort(404)


@app.route('/jobs', methods=['GET'])
def filter_jobs():
    # params: author, status, tank_name
    # author or tank_name is required
    DEFAULT_LIMIT = 64
    author, status, tank_name, limit = request.args.get('author'), request.args.get('status'),\
                                       request.args.get('tank_name'), request.args.get('limit', DEFAULT_LIMIT)
    if status is not None:
        try:
            status = Status(status.lower())
        except ValueError as e:
            return abort(400, '{}.\nAvailable statuses are {}'.format(e, [s.value for s in list(Status)]))
    try:
        limit = int(limit)
    except ValueError:
        limit = DEFAULT_LIMIT
    args = {'author': author, 'status': status, 'claimed_by': tank_name}
    prepared_args = {k: v for k, v in args.items() if v is not None}
    tasks = Task.query.filter_by(**prepared_args).limit(limit).all()
    return flask.jsonify([task.as_dict() for task in tasks])


STDOUT_FMT = r'^(\d+)\.?\d*:: (.+)$'


@app.route('/job/<int:jobid>/stdout', methods=['POST'])
def set_job_stdout(jobid: int):
    key_date = datetime.date.today().isoformat()

    def replacement(matchobj):
        ts, line = matchobj.group(1, 2)
        escaped_line = line.replace('\'', '\\\'')
        return "('{}', '{}', {}, '{}')".format(key_date, jobid, ts, escaped_line)

    data = [re.sub(STDOUT_FMT, replacement, line)  # e.g. '1537896294231:: some log message'
            for line in filter_fmt(filter(lambda l: len(l) > 0,
                                          request.data.decode('utf-8').split('\n')),
                                   STDOUT_FMT)]
    query = "insert into bdk.logs_buffer VALUES {}".format(', '.join(data))
    r = requests.post(app.config['CC_URL'], data=query.encode('utf8'))
    app.logger.info(r.content)
    r.raise_for_status()
    return flask.jsonify({'success': True})


def filter_fmt(l, regex):
    for line in l:
        if re.match(regex, line) is not None:
            yield line
        else:
            app.logger.warning('Format mismatch: {}'.format(line))


@app.route('/job/<int:task_id>/finish.json', methods=['POST'])
def finish_task(task_id):
    rc = request.json.get('return code')
    task = Task.query.get(task_id)
    if task is not None:
        task.return_code = rc
        task.status = Status.FINISHED
        task.finished_at = datetime.datetime.utcnow()
        database.update(task)
        return flask.jsonify({'success': True})
    else:
        abort(404)


@app.route('/<handler>.<fmt>', methods=('POST',))
def handler(handler, fmt):
    formatter = Formatter('yaml')

    handlers = {
        'enqueue': EnqueueHandler,
        'claim': ClaimHandler,
        'finish': PatchHandler,
    }

    try:
        assert request.data, 'empty__request__data'
        request_data = formatter.load(request.data)
        print(request_data)
        result = handlers[handler](request_data).handle()
        return Response(formatter.dump(result.data), status=result.status_code, mimetype=f'application/{fmt}')
    except KeyError as exc:
        return Response(formatter.dump(repr(exc)), status=404, mimetype=f'application/{fmt}')
    except YAMLError as exc:
        app.logger.error(exc)
        return Response(formatter.dump(repr(exc)), status=400, mimetype=f'application/{fmt}')
    except AssertionError as exc:
        app.logger.error(exc)
        return Response(formatter.dump(repr(exc)), status=400, mimetype=f'application/{fmt}')


@app.route('/dc')
def dc():
    return Response(DCFinder().get_dc(request.args.get('host')), status=200)


class AbstractHandler:
    class Result:
        status_code = None
        data = ''

    def __init__(self, request_data: dict):
        self.request_data = request_data

    def handle(self):
        """

        :return: self.Result
        """
        raise NotImplementedError


class EnqueueHandler(AbstractHandler):
    def handle(self):
        """
        demands:
          host_name: 'netort-incredible-bdk'
          executor_type: 'volta'
          executor_tag:
            - 'volta-netort'
            - 'volta-tort'
          executor_phone_model: some_phone_model
        :return:
        """
        author = str(self.request_data.get('author'))
        try:
            config = self.request_data.get('config')
            assert config is not None, 'config__is__mandatory'
            configinitial = deepcopy(config)
            demands = self.request_data.get('demands')
            assert demands, 'demands__are__mandatory'

            # TODO: validate demands
            executor_type = demands.get('executor_type')
            assert executor_type, 'demands.executor_type__is__mandatory'
            assert executor_type in ('volta', 'phantom', 'jmeter', 'bfg', 'fastlane'), \
                'demands.executor_type__must__be_one_of__volta,phantom,jmeter,bfg,fastlane'
            if executor_type in ('phantom', 'jmeter', 'bfg'):
                dc = demands.get('dc')
                assert dc in DCs, 'demands.dc__must__be_one_of__sas,man,vla,myt,iva'
            # default_config = fetch_default_config(executor_type)
            # TODO: validate config
            # result = validate_config(user_config=config, default_config=default_config)
            # assert result['success'], result['error']
            # config = result['config']
            task = Task(
                configinitial=configinitial,
                config=config,
                demands=demands,
                author=author
            )
            database.create(task)
            self.Result.status_code = 201
            self.Result.data = {
                'configinitial': task.configinitial,
                'task_id': task.id,
                'author': task.author
            }
        except AssertionError as exc:
            self.Result.status_code = 400
            self.Result.data = repr(exc)
        except requests.exceptions.HTTPError as exc:
            app.logger.error(exc)
            self.Result.status_code = exc.request.status_code
            self.Result.data = repr(exc)
        except Exception as exc:
            app.logger.exception(exc)
            self.Result.status_code = 500
            self.Result.data = repr(exc)

        return self.Result


class ClaimHandler(AbstractHandler):
    def handle(self):
        """
        capabilities:
          host_name: 'netort-incredible-bdk'
          executor_type: 'volta'
          executor_tag:
            - 'volta-netort'
            - 'volta-tort'
          executor_phone_model: some_phone_model
        :return:
        """
        # TODO: validate capabilities
        capabilities = self.request_data.get('capabilities', {})
        executor_type = capabilities.get('executor', {}).get('type', capabilities.get('executor_type'))
        app.logger.info('Claim request from host {}\nUser-agent: {}\nExecutor type: {}\nCapabilities: {}'.
                        format(request.remote_addr, request.user_agent, executor_type, capabilities))
        assert executor_type, 'capabilities.executor_type__is__mandatory'
        assert executor_type in ('volta', 'phantom', 'jmeter', 'bfg', 'fastlane'), \
            'capabilities.executor_type__must__be_one_of__volta,phantom,jmeter,bfg,fastlane'

        app.logger.error('FINDING_DC')
        if executor_type in ('phantom', 'jmeter', 'bfg'):
            if not capabilities.get('dc') in DCs:
                capabilities['dc'] = DCFinder().get_dc(capabilities.get('__fqdn', request.remote_addr))

        executor_id = capabilities.get('__fqdn') \
                      or capabilities.get('host_fqnd') \
                      or capabilities.get('host_name') \
                      or capabilities.get('executor_tag') \
                      or capabilities.get('executor_id') \
                      or request.remote_addr

        restrictions = defaultdict(list)

        # ==== С ЭТОГО МЕСТА ЗНАЧЕНИЯ КАПАБИЛИТИЗ ПРЕВРАЩАЮТСЯ В СПИСИКИ (чтобы поддержать логику "ИЛИ")

        for k in capabilities.keys():
            capabilities[k] = [capabilities[k]]
            for v in capabilities[k]:
                if isinstance(v, str) and v.startswith('+'):
                    restrictions[k].append(v.lstrip('+ '))
                    capabilities[k][capabilities[k].index(v)] = v.lstrip('+ ')

        # # executor_fqdn = capabilities.get('host', {}).get('fqdn')
        # executor_fqdn = self.request_data.get('tank')
        # executor_dc = capabilities.get('host', {}).get('dc')
        # executor_type = capabilities.get('executor', {}).get('type')
        # capabilities['host']['dc'] = check_tank_dc(executor_fqdn, executor_dc)
        # # FIND TASK

        app.logger.error('FINDING_TASKS')
        # TODO: как-то избежать проверки по всем существующим таскам
        tasks = Task.query.filter_by(claimed_by=None)
        task_to_claim = None
        for task in tasks:
            try:
                demands = task.demands.copy()
                assert set(demands.keys()) <= set(capabilities.keys())
                for k in demands.keys():
                    if not isinstance(demands[k], list):
                        demands[k] = [demands[k]]
                    assert any([cv in demands[k] for cv in capabilities[k]])
                for k in restrictions.keys():
                    assert all([rv in demands[k] for rv in restrictions[k]])
                task_to_claim = task
            except AssertionError:
                continue
            except Exception as exc:
                app.logger.error(repr(exc))

        if task_to_claim:
            executor_id = f'[{DCFinder._get_fqdn(request.remote_addr)}] {executor_id}'
            claim_task(task_to_claim, executor_id)
            self.Result.status_code = 200
            self.Result.data = {
                'config': task_to_claim.config,
                'task_id': task_to_claim.id
            }
        else:
            self.Result.status_code = 404
            self.Result.data = 'No tasks for you!'

        return self.Result


class PatchHandler(AbstractHandler):
    def handle(self):
        """
        task_id:
        status:
        :return:
        """
        try:
            task_id = self.request_data.get('task_id')
            status = self.request_data.get('status')
            assert task_id, 'task_id__is__mandatory'
            assert status, 'status__is__mandatory'
            task = Task.query.get(task_id)
            task.status = status
            database.update(task)
            self.Result.status_code = 200
        except AssertionError as exc:
            self.Result.status_code = 400
            self.Result.data = repr(exc)
        except requests.exceptions.HTTPError as exc:
            app.logger.error(exc)
            self.Result.status_code = exc.request.status_code
            self.Result.data = repr(exc)
        except Exception as exc:
            app.logger.exception(exc)
            self.Result.status_code = 500
            self.Result.data = repr(exc)

        return self.Result
