from functools import partial
import re
import six

from flask import Flask, request, abort
from lxml import etree as ET

from yandex.maps.wiki import db, fastcgihelpers as fh
from yandex.maps.wiki.pgpool3 import get_pgpool
from yandex.maps.wiki.tasks import task_types, EM, states
from yandex.maps.wiki.tasks.models import Task
from maps.wikimap.mapspro.libs.python import acl
import maps.wikimap.mapspro.services.tasks.fastcgi.lib.task_type_parameters as ttp
from maps.wikimap.mapspro.services.tasks.fastcgi.lib import schedule

from importlib import import_module

MODULES = [
    'apply_shadow_attributes',
    'badge',
    'diffalert',
    'export',
    'groupedit',
    'guide_pedestrian',
    'import',
    'import_feedback',
    'prepare_stable_branch',
    'reject_feedback',
    'releases_notification',
    'validation',
    'validation_export',
    'validation_feedback_converter',
    'vrevisions_refresh']

for module_name in MODULES:
    import_module('maps.wikimap.mapspro.services.tasks.fastcgi.modules.' + module_name + '.' + module_name)

ALLOW_ORIGIN_REGEX = re.compile('.*\\.yandex\\.(ru|net)')

ACL_GROUP = 'mpro'


def check_acl(request):
    if 'uid' not in request.values:
        return  # assuming frontend always provides uid, backend never provides one

    uid = int(request.values['uid'])
    if not acl.is_member_of_group(get_pgpool(db.CORE_DB), ACL_GROUP, uid):
        raise fh.ServiceException('forbidden', status='ERR_FORBIDDEN')


tasks = Flask(__name__)

for name, task_type in task_types.items():
    blueprint = getattr(task_type, 'flask_blueprint', None)
    if blueprint:
        tasks.register_blueprint(blueprint, url_prefix='/' + name)

tasks.register_blueprint(schedule.blueprint, url_prefix='/schedule')


@tasks.errorhandler(404)
@tasks.errorhandler(409)
@tasks.errorhandler(400)
@tasks.errorhandler(Exception)
@fh.crossdomain(origin_regex=ALLOW_ORIGIN_REGEX, headers='content-type')
def handle_exception(ex):
    return fh.error_handler(ex, EM.tasks())


@tasks.route('/ping')
def ping():
    return "ok"


def get_task_type(type_id):
    if type_id not in task_types:
        abort(404, 'task type: %s not found' % type_id)
    return task_types[type_id]


def get_task_by_id(session, task_id, for_update=False):
    query = session.query(Task)
    if for_update:
        query = query.with_lockmode('update')
    task = query.get(task_id)
    if task is None:
        abort(404, 'task id: %s not found' % task_id)
    return task


read_session_sync = partial(db.read_session_sync, token_getter=fh.token_from_request)


@tasks.route('/capabilities')
def capabilities(type_id=None):
    check_acl(request)

    type_id = request.values.get('type')
    if type_id is None:
        tt_elements = [EM.task_type(tt.capabilities_ET(), id=id)
                       for id, tt in task_types.items()]
    else:
        tt_elements = [EM.task_type(
            get_task_type(type_id).capabilities_ET(), id=type_id)]

    return fh.xml_response(EM.tasks(EM.capabilities(*tt_elements)))


@tasks.route('/tasks', methods=['POST', 'OPTIONS'])
@fh.crossdomain(origin_regex=ALLOW_ORIGIN_REGEX, headers='content-type')
@db.write_session('core')
def new_task(session):
    check_acl(request)

    uid = int(request.values['uid'])
    type_id = request.values['type']
    task_type = get_task_type(type_id)
    task = task_type.create(uid, request)
    session.add(task)
    session.commit()
    result = task_type.launch(session, task.id, request)
    task.grinder_task_id = result.id

    session.commit()
    response = fh.xml_response(EM.tasks(task.get_ET_brief(),
                                        EM.internal(EM.token(db.get_token(session)))))
    return response


@tasks.route('/tasks')
@read_session_sync('core')
def list_tasks(session):
    check_acl(request)

    page = int(request.values.get('page', 1))
    per_page = int(request.values.get('per-page', 10))

    query = session.query(Task).order_by(Task.created.desc())
    if 'type' in request.values:
        query = query.filter(Task.type == request.values['type'])
    if 'created-by' in request.values:
        query = query.filter(Task.created_by == int(request.values['created-by']))
    if 'parent' in request.values:
        query = query.filter(Task.parent_id == int(request.values['parent']))

    if 'since' in request.values:
        query = query.filter(Task.created >= request.values['since'])
    if 'till' in request.values:
        query = query.filter(Task.created <= request.values['till'])

    page = fh.correct_page(page, per_page, query.count())
    offset = (page - 1) * per_page

    return fh.xml_response(EM.tasks(
        EM.task_list(page=page,
                     per_page=per_page,
                     total_count=query.count(),
                     *[t.get_ET_brief() for t in query[offset:offset + per_page]])))


@tasks.route('/tasks/<task_id>')
@read_session_sync('core')
def get_task(session, task_id):
    check_acl(request)

    task = get_task_by_id(session, task_id)

    params = dict((k.replace('-', '_'), v) for k, v in six.iteritems(request.values))
    return fh.xml_response(EM.tasks(task.get_ET_full(**params)))


@tasks.route('/tasks/<task_id>/log')
@read_session_sync('core')
def get_log(session, task_id):
    check_acl(request)

    return get_task_by_id(session, task_id).log


@tasks.route('/tasks/<task_id>/parameters', methods=['PUT'])
@db.write_session('core')
def change_task_parameters(session, task_id):
    check_acl(request)

    return fh.xml_response(
        get_task_by_id(session, task_id, for_update=True)
            .change_parameters(session, request))


@tasks.route('/tasks/<task_id>', methods=['PUT'])
@db.write_session('core')
def set_state(session, task_id):
    check_acl(request)

    task = get_task_by_id(session, task_id, for_update=True)

    state = request.values['status'].upper()
    if state == states.REVOKED:
        if not task.revocable:
            raise fh.ServiceException('task id: %s not revocable' % task_id,
                                      status='TASK_NOT_REVOCABLE')
        task.set_state(states.REVOKED)
        task.revoke()
    elif state == states.PENDING:
        if not task.resumable:
            raise fh.ServiceException('task id: %s not resumable' % task_id,
                                      status='TASK_NOT_RESUMABLE')
        task.set_state(states.PENDING)
        task.resume()
    else:
        abort(400, "can't set status to '%s'" % state)

    uid = int(request.values['uid'])
    task.on_modify(uid)
    return fh.xml_response(EM.tasks(task.get_ET_brief(),
                                    EM.internal(EM.token(db.get_token(session)))))


@tasks.route('/types/<type_id>/parameters')
@read_session_sync('core')
def get_task_type_parameters(session, type_id):
    check_acl(request)

    if type_id not in task_types:
        abort(404, 'task type: %s not found' % type_id)

    attrs = ttp.get_task_type_parameters(session, type_id)
    return fh.xml_response(EM.parameters(
        *[EM.parameter(key=k, value=v) for k, v in attrs.items()]))


@tasks.route('/types/<type_id>/parameters', methods=['PUT'])
@db.write_session('core')
def concatenate_task_type_parameters(session, type_id):
    check_acl(request)

    if type_id not in task_types:
        abort(404, 'task type: %s not found' % type_id)

    root = ET.fromstring(request.data)
    hstore = {}
    for child in root:
        (key, value) = (child.attrib['key'], child.attrib['value'])

        if "'" in key:
            raise ValueError('Wrong key')
        if "'" in value:
            raise ValueError('Wrong value')

        hstore[key] = value

    ttp.concatenate_task_type_parameters(session, type_id, hstore)

    attrs = ttp.get_task_type_parameters(session, type_id)
    return fh.xml_response(EM.parameters(
        *[EM.parameter(key=k, value=v) for k, v in attrs.items()]))
