from functools import partial
import logging
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
from lxml import etree as ET
import re

from flask import Blueprint, request

from yandex.maps.wiki import db
from yandex.maps.wiki import config
from yandex.maps.wiki.tasks import grinder, EM, TASKS_NAMESPACE
from yandex.maps.wiki.tasks.models import Base
from yandex.maps.wiki import utils
from yandex.maps.wiki.utils import require, string_to_bool
from yandex.maps.wiki import fastcgihelpers as fh

NAMESPACES = {'mpt': TASKS_NAMESPACE}

ALL_DAYS_BITMASK = 0x7f

blueprint = Blueprint('schedule', __name__)

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


class ScheduleItem(Base):
    __tablename__ = 'task_schedule'
    __table_args__ = {'schema': 'service'}

    id = sa.Column(sa.BigInteger, primary_key=True)
    name = sa.Column(sa.String)
    cron_task_name = sa.Column(sa.String)
    created_at = sa.Column(sa.DateTime)
    created_by = sa.Column(sa.BigInteger)
    cron_expr = sa.Column(sa.String)
    task_type = sa.Column(sa.String)
    task_params = sa.Column(postgresql.HSTORE)

    @sa.orm.validates('name')
    def validate_name(self, key, name):
        name = name.strip()
        require(name,
                fh.ServiceException('Empty name',
                                    status='ERR_BAD_REQUEST'))
        return name


def parse_validation_params(xml_node):
    '''Parse xml and return params for validation task'''
    params_node = xml_node.find('/mpt:request-save-schedule-item/mpt:validation-params',
                                namespaces=NAMESPACES)

    branch = params_node.find("mpt:branch-type", namespaces=NAMESPACES).text
    preset = params_node.find("mpt:preset", namespaces=NAMESPACES).text

    params = {'branch' : branch, 'preset' : preset}

    aoi_node = params_node.find("mpt:aoi", namespaces=NAMESPACES)
    if aoi_node is not None:
        params['aoi'] = aoi_node.text

    region_node = params_node.find("mpt:region", namespaces=NAMESPACES)
    if region_node is not None:
        params['region'] = region_node.text

    return params


def validation_params_to_xml(params):
    '''Create xml with validation params from hstore'''
    ret = EM.validation_params(
        EM.branch_type(params['branch']),
        EM.preset(params['preset']))

    if 'aoi' in params:
        ret.append(EM.aoi(params['aoi']))
    if 'region' in params:
        ret.append(EM.region(params['region']))

    return ret


def day_time_to_cron_expr(day, time):
    '''Convert day of the week and time (%H:%M) to cron expr string
    Day is a bitmask. Example: '1010000' - Mo, We'''
    day = int(day)
    require(day >= 1 and day <= ALL_DAYS_BITMASK,
            fh.ServiceException('Wrong day value {0}'.format(day),
                                status='ERR_BAD_REQUEST'))

    dow = "*"
    if day != ALL_DAYS_BITMASK:
        dow = ",".join([str((d + 1) % 7) for d in range(7) if (1 << d) & day])

    (hours, minutes) = map(int, time.split(':'))

    require(minutes >= 0 and minutes < 60,
            fh.ServiceException('Wrong minutes value {0}'.format(minutes),
                                status='ERR_BAD_REQUEST'))
    require(hours >= 0 and hours < 24,
            fh.ServiceException('Wrong hours value {0}'.format(hours),
                                status='ERR_BAD_REQUEST'))

    return "{0:d} {1:d} * * {2}".format(minutes, hours, dow)


def cron_expr_to_day_time(cron_expr):
    '''Convert cron_expr to tuple of two strings: bitmask day of the week and time (%H:%M)'''
    parts = cron_expr.split()
    minutes = int(parts[0])
    hours = int(parts[1])
    dow = parts[4]
    day = ALL_DAYS_BITMASK if dow == "*" else sum(pow(2, (d + 6) % 7) for d in map(int, dow.split(',')))
    return (str(day), "{0:02d}:{1:02d}".format(hours, minutes))


def get_ET_brief(shedule_item, crontabs):
    return EM.schedule_item(
        EM.name(shedule_item.name),
        EM.task_type(shedule_item.task_type),
        id=shedule_item.id,
        created_by=shedule_item.created_by,
        created=shedule_item.created_at,
        enabled=(shedule_item.cron_task_name in crontabs))


def get_ET_full(shedule_item, crontab):
    day, time = cron_expr_to_day_time(crontab['cron_expr'] if crontab is not None else shedule_item.cron_expr)

    return EM.schedule_item(
        EM.name(shedule_item.name),
        EM.start_time(day=day, time=time),
        EM.task_type(shedule_item.task_type),
        validation_params_to_xml(shedule_item.task_params),
        id=shedule_item.id,
        created_by=shedule_item.created_by,
        created=shedule_item.created_at,
        enabled=(crontab is not None))


@blueprint.route('/items', methods=['GET'])
@read_session_sync(db.CORE_DB)
def get_items(session):
    '''Return brief schedule items of the user'''
    page = int(request.values.get('page', 1))
    per_page = int(request.values.get('per-page', 10))

    try:
        gateway = grinder.GrinderGateway(config.get_config().grinder_params.host)
        crontabs = gateway.crontabs()
    except grinder.GrinderError as err:
        logging.exception(err)
        raise fh.ServiceException('Grinder error',
                                  status='ERR_GRINDER_ERROR')

    query = session.query(ScheduleItem).order_by(ScheduleItem.id.desc())
    if 'created-by' in request.values:
        created_by = int(request.values['created-by'])
        query = query.filter(ScheduleItem.created_by == created_by)

    total_count = query.count()

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

    return fh.xml_response(
        EM.tasks(
            EM.response_schedule_items(
                EM.schedule_items(
                    page=page,
                    per_page=per_page,
                    total_count=total_count,
                    *[get_ET_brief(item, crontabs) for item in query[offset:offset + per_page]]))))


@blueprint.route('/items/<id>', methods=['GET'])
@read_session_sync(db.CORE_DB)
def get_item(session, id):
    '''Return one full schedule item'''
    item = session.query(ScheduleItem).get(id)
    require(item,
            fh.ServiceException("Schedule item '{0}' is not found".format(id),
                                status='ERR_MISSING_SCHEDULE_ITEM'))

    crontab = None
    try:
        gateway = grinder.GrinderGateway(config.get_config().grinder_params.host)
        crontab = gateway.crontab(item.cron_task_name)
    except grinder.CrontabNotFound:
        pass  # it is ok => enabled==False
    except grinder.GrinderError as err:
        logging.exception(err)
        raise fh.ServiceException('Grinder error',
                                  status='ERR_GRINDER_ERROR')

    return fh.xml_response(
        EM.tasks(
            EM.response_schedule_item(
                get_ET_full(item, crontab))))


@blueprint.route('/items', methods=['POST'])
@db.write_session(db.CORE_DB)
def create_item(session):
    '''Create new schedule item'''
    item = ScheduleItem()
    item.name = request.values['name']
    item.created_by = int(request.values['uid'])
    item.created_at = utils.utcnow()
    item.cron_expr = day_time_to_cron_expr(request.values['day'], request.values['time'])
    item.task_type = request.values['type']
    item.task_params = parse_validation_params(ET.parse(request.stream))

    try:
        session.add(item)
        session.commit()
    except sa.exc.IntegrityError as err:
        logging.exception(err)
        raise fh.ServiceException('Duplicate item name {0}'.format(item.name),
                                  status='ERR_DUPLICATE_SCHEDULE_NAME')

    crontab = None

    enabled = string_to_bool(request.values['enabled'])
    if enabled:
        try:
            args = {'type' : 'submit-editor-task',
                    'uid' : item.created_by,
                    'task-type' : item.task_type,
                    'params' : item.task_params}

            gateway = grinder.GrinderGateway(config.get_config().grinder_params.host)
            crontab = gateway.put_crontab(cron_task_name=item.cron_task_name,
                                          args=args,
                                          cron_expr=item.cron_expr)
        except grinder.GrinderError as err:
            logging.exception(err)
            raise fh.ServiceException('Grinder error',
                                      status='ERR_GRINDER_ERROR')

    return fh.xml_response(
        EM.tasks(
            EM.response_save_schedule_item(
                get_ET_full(item, crontab)),
            EM.internal(EM.token(db.get_token(session)))))


@blueprint.route('/items/<id>', methods=['PUT'])
@db.write_session(db.CORE_DB)
def change_item(session, id):
    '''Change existing schedule item'''
    uid = int(request.values['uid'])

    item = session.query(ScheduleItem).get(id)
    require(item,
            fh.ServiceException("Schedule item '{0}' is not found".format(id),
                                status='ERR_MISSING_SCHEDULE_ITEM'))
    require(item.created_by == uid,
            fh.ServiceException("User '{0}' is not allowed to edit schedule item '{1}'"
                                .format(uid, item.cron_task_name),
                                status='ERR_FORBIDDEN'))
    require(item.task_type == request.values['type'],
            fh.ServiceException("It is not allowed to edit task type '{0}'"
                                .format(item.task_type),
                                status='ERR_FORBIDDEN'))

    item.name = request.values['name']
    item.cron_expr = day_time_to_cron_expr(request.values['day'], request.values['time'])
    item.task_params = parse_validation_params(ET.parse(request.stream))

    enabled = string_to_bool(request.values['enabled'])

    crontab = None
    try:
        gateway = grinder.GrinderGateway(config.get_config().grinder_params.host)
        if enabled:
            args = {'type' : 'submit-editor-task',
                    'uid' : item.created_by,
                    'task-type' : item.task_type,
                    'params' : item.task_params}

            crontab = gateway.put_crontab(cron_task_name=item.cron_task_name,
                                          args=args,
                                          cron_expr=item.cron_expr)
        else:
            gateway.delete_crontab(cron_task_name=item.cron_task_name)

    except grinder.CrontabNotFound:
        pass  # it is ok => enabled==False
    except grinder.GrinderError as err:
        logging.exception(err)
        raise fh.ServiceException('Grinder error',
                                  status='ERR_GRINDER_ERROR')

    try:
        session.commit()
    except sa.exc.IntegrityError as err:
        logging.exception(err)
        raise fh.ServiceException('Duplicate item name {0}'.format(request.values['name']),
                                  status='ERR_DUPLICATE_SCHEDULE_NAME')

    return fh.xml_response(
        EM.tasks(
            EM.response_save_schedule_item(
                get_ET_full(item, crontab)),
            EM.internal(EM.token(db.get_token(session)))))


@blueprint.route('/items/<id>', methods=['DELETE'])
@db.write_session(db.CORE_DB)
def delete_item(session, id):
    '''Delete schedule item from the database and from grinder'''
    uid = int(request.values['uid'])

    item = session.query(ScheduleItem).get(id)
    require(item,
            fh.ServiceException("Schedule item '{0}' is not found".format(id),
                                status='ERR_MISSING_SCHEDULE_ITEM'))
    require(item.created_by == uid,
            fh.ServiceException("User '{0}' is not allowed to delete schedule item '{1}'"
                                .format(uid, item.cron_task_name),
                                status='ERR_FORBIDDEN'))

    session.delete(item)

    try:
        gateway = grinder.GrinderGateway(config.get_config().grinder_params.host)
        gateway.delete_crontab(cron_task_name=item.cron_task_name)
    except grinder.CrontabNotFound:
        pass  # it is ok => enabled==False
    except grinder.GrinderError as err:
        logging.exception(err)
        raise fh.ServiceException('Grinder error',
                                  status='ERR_GRINDER_ERROR')

    session.commit()

    return fh.xml_response(
        EM.tasks(
            EM.response_delete_schedule_item(),
            EM.internal(EM.token(db.get_token(session)))))


@blueprint.route('/check', methods=['GET'])
@db.read_session(db.CORE_DB)
def check_crontabs(session):
    '''Check if there are crontabs in the grinder that do not exist in the DB'''
    existing_cron_task_names = [item.cron_task_name for item in session.query(ScheduleItem)]

    try:
        gateway = grinder.GrinderGateway(config.get_config().grinder_params.host)
        crontabs = gateway.crontabs()
        for cron_task_name in crontabs:
            if re.match('^tasks_', cron_task_name):
                if cron_task_name not in existing_cron_task_names:
                    return "dangling cron_task_name {0}".format(cron_task_name)

    except grinder.GrinderError as err:
        logging.exception(err)
        return "grinder error"

    return "ok"
