# coding: utf-8
import logging
import geoalchemy2 as ga
import geoalchemy2.shape
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql as psql_sa
from shapely import wkb
import contextlib
import tarfile
import base64
import os
import tempfile
import shutil

from six import StringIO

from osgeo import ogr
from osgeo import osr

from lxml import etree as ET
from sqlalchemy.ext import hybrid

from yandex.maps.wiki import db, fastcgihelpers as fh
from yandex.maps.wiki import tasks
from yandex.maps.wiki import config
from yandex.maps.wiki import pgpool3
from yandex.maps.wiki.tasks import EM
from yandex.maps.wiki.tasks import models

from maps.wikimap.mapspro.libs.python import acl

TASK_NAME = 'releases_notification'
SRID = 3395
ACL_PATH = 'mpro/tasks/mail-broadcast'

RELEASE_TYPES = (TYPE_SAT, TYPE_VEC, TYPE_SURVEY, TYPE_NEWS, TYPE_EVENT) = (
    'sat', 'vec', 'survey', 'news', 'event')
MODE_TYPES = (MODE_TEST, MODE_DRY, MODE_REAL) = ('test', 'dry', 'real')


@tasks.register_task_type(name=TASK_NAME)
class ReleasesNotification:
    @staticmethod
    def capabilities_ET():
        return tasks.EM.releases_notification_task_type()

    @staticmethod
    def create(uid, request):
        try:
            if not acl.is_permission_granted(
                    pgpool3.get_pgpool(db.CORE_DB), ACL_PATH, uid, None):
                raise fh.ServiceException('forbidden', status='ERR_FORBIDDEN')

            xml = ET.parse(request.stream)

            release_type = xml.xpath('/releases-notification-task/release-type')[0].text
            task = ReleasesNotificationTask()
            if release_type == TYPE_VEC:
                task.vec_param = ReleasesNotificationVecParam()
            elif release_type == TYPE_SAT:
                param = ReleasesNotificationSatParam()
                param.geom = xml.xpath('/releases-notification-task/geometry/data')[0].text
                task.sat_param = param
            elif release_type == TYPE_SURVEY:
                param = ReleasesNotificationSurveyParam()
                param.subject = xml.xpath('/releases-notification-task/subject')[0].text
                task.survey_param = param
            elif release_type == TYPE_EVENT:
                param = ReleasesNotificationEventParam()
                param.subject = xml.xpath('/releases-notification-task/subject')[0].text
                param.event_name = xml.xpath('/releases-notification-task/event-name')[0].text
                task.event_param = param

            if release_type not in RELEASE_TYPES:
                raise fh.ServiceException(
                    'Unrecognized release type: %s' % release_type,
                    status='ERR_BAD_REQUEST')

            task.release_type = release_type

            blog_url = xml.xpath('/releases-notification-task/blog-url')[0].text
            for symbol in ('<', '>', '"'):
                if symbol in blog_url:
                    raise fh.ServiceException(
                        'Unsupported symbol {0!r} in blog url {1}'.format(symbol, blog_url),
                        status='ERR_BAD_REQUEST')
            task.blog_url = blog_url

            task.mode = xml.xpath('/releases-notification-task/mode')[0].text
            if task.mode not in MODE_TYPES:
                raise fh.ServiceException(
                    'Unrecognized mode: %s' % task.mode,
                    status='ERR_BAD_REQUEST')

            test_emails = [
                emailElem.text for emailElem in xml.xpath('/releases-notification-task/emails/email')
            ]
            for test_email in test_emails:
                for symbol in (',', '"', "'"):
                    if symbol in test_email:
                        raise fh.ServiceException(
                            'Unsupported symbol {0!r} in email {1}'.format(symbol, test_email),
                            status='ERR_BAD_REQUEST')
            task.test_emails = test_emails

        except fh.ServiceException:
            raise
        except Exception:
            logging.exception('invalid request')
            raise fh.ServiceException('error while parsing request',
                                      status='ERR_BAD_REQUEST')

        task.on_create(uid)
        return task

    @staticmethod
    def launch(session, task_id, request):
        task = session.query(ReleasesNotificationTask).get(task_id)

        gateway = tasks.grinder.GrinderGateway(config.get_config().grinder_params.host)
        return gateway.submit(task.get_grinder_args())


class ReleasesNotificationTask(models.Task):
    __tablename__ = 'releases_notification_task'
    __table_args__ = {'schema': 'service'}
    __mapper_args__ = {'polymorphic_identity': 'releases_notification'}

    id = sa.Column(sa.BigInteger, sa.ForeignKey('service.task.id'), primary_key=True)
    release_type = sa.Column(sa.String, nullable=False)
    blog_url = sa.Column(sa.String, nullable=False)
    mode = sa.Column(sa.String, nullable=False)
    sent_notifications_num = sa.Column(sa.BigInteger)
    test_emails = sa.Column(psql_sa.ARRAY(sa.String))
    vec_param = sa.orm.relation(
        'ReleasesNotificationVecParam',
        backref="releases_notification_vec_param",
        uselist=False,
        cascade="all")
    sat_param = sa.orm.relation(
        'ReleasesNotificationSatParam',
        backref="releases_notification_sat_param",
        uselist=False,
        cascade="all")
    survey_param = sa.orm.relation(
        'ReleasesNotificationSurveyParam',
        backref="releases_notification_survey_param",
        uselist=False,
        cascade="all")
    event_param = sa.orm.relation(
        'ReleasesNotificationEventParam',
        backref="releases_notification_event_param",
        uselist=False,
        cascade="all")

    @property
    def _param(self):
        type_to_param = {
            TYPE_VEC: self.vec_param,
            TYPE_SAT: self.sat_param,
            TYPE_SURVEY: self.survey_param,
            TYPE_EVENT: self.event_param,
        }
        return type_to_param.get(self.release_type)

    def context_ET_brief(self, *args, **kwargs):
        """Context for POST request"""
        root_elem = EM.releases_notification_context(
            EM.release_type(self.release_type),
            EM.blog_url(self.blog_url),
            EM.mode(self.mode))
        if self.mode == MODE_TEST:
            elem_emails = EM.emails(*[EM.email(r) for r in self.test_emails])
            root_elem.append(elem_emails)

        if self._param:
            self._param.dump_to_xml(root_elem)

        return root_elem

    def context_ET_full(self, *args, **kwargs):
        """Context for GET request"""
        root_elem = EM.releases_notification_context(
            EM.release_type(self.release_type),
            EM.blog_url(self.blog_url),
            EM.mode(self.mode))

        if self._param:
            self._param.dump_to_xml(root_elem)

        return root_elem

    def result_ET_brief(self, *args, **kwargs):
        root_elem = EM.releases_notification_result()
        if self.sent_notifications_num is not None:
            root_elem.append(EM.emails_sent(self.sent_notifications_num))
        else:
            root_elem.append(EM.emails_sent(0))
        return root_elem

    def get_grinder_args(self):
        """
        Returns args of task for grinder
        Note that file for sat releases is not included into args
        and passes through the db
        """
        return {
            'type': TASK_NAME,
            'task-id': self.id,
        }

    def resume(self):
        gateway = tasks.grinder.GrinderGateway(config.get_config().grinder_params.host)
        result = gateway.submit(self.get_grinder_args())
        self.grinder_task_id = result.id


class ReleasesNotificationVecParam(models.Base):
    __tablename__ = 'releases_notification_vec_param'
    __table_args__ = {'schema': 'service'}

    param_id = sa.Column(sa.BigInteger, primary_key=True)
    task_id = sa.Column(sa.BigInteger,
                        sa.ForeignKey(ReleasesNotificationTask.id))

    def dump_to_xml(self, root_elem):
        pass


def to_db_geom(val, work_dir):
    binary_data = base64.b64decode(val)
    tfile = StringIO(binary_data)
    with contextlib.closing(tarfile.open(fileobj=tfile, mode='r:gz')) as archive:
        if not os.path.exists(work_dir):
            os.mkdir(work_dir)
        task_work_dir = tempfile.mkdtemp(dir=work_dir)
        try:
            archive.extractall(path=task_work_dir)
            data = ogr.Open(task_work_dir, 0)
            layer = data.GetLayer(0)
            feat = layer.GetNextFeature()
            geom_ref = feat.GetGeometryRef()
            sourceSpatialRef = geom_ref.GetSpatialReference()

            targetSpatialRef = osr.SpatialReference()
            targetSpatialRef.ImportFromEPSG(SRID)

            transformation = osr.CoordinateTransformation(sourceSpatialRef, targetSpatialRef)
            geom_ref.Transform(transformation)

            shape = wkb.loads(geom_ref.ExportToWkb())
            return ga.shape.from_shape(shape, srid=SRID)
        finally:
            shutil.rmtree(task_work_dir, ignore_errors=True)


class ReleasesNotificationSatParam(models.Base):
    __tablename__ = 'releases_notification_sat_param'
    __table_args__ = {'schema': 'service'}

    param_id = sa.Column(sa.BigInteger, primary_key=True)
    task_id = sa.Column(sa.BigInteger,
                        sa.ForeignKey(ReleasesNotificationTask.id))

    _geom = sa.Column('geom', ga.Geometry('POLYGON', srid=SRID))

    @hybrid.hybrid_property
    def geom(self):
        if self._geom is None:
            return None
        return ga.shape.to_shape(self._geom)

    @geom.setter
    def geom(self, val):
        """Convert geometry into psql format and save it
        Available input format - base64 encoded tgz archive of shape file directory
        """
        self._geom = to_db_geom(val, config.get_config().releases_notification_params.work_dir)

    @geom.expression
    def geom(cls):
        return cls._geom

    def dump_to_xml(self, root_elem):
        pass


class ReleasesNotificationSurveyParam(models.Base):
    __tablename__ = 'releases_notification_survey_param'
    __table_args__ = {'schema': 'service'}

    param_id = sa.Column(sa.BigInteger, primary_key=True)
    task_id = sa.Column(sa.BigInteger,
                        sa.ForeignKey(ReleasesNotificationTask.id))
    subject = sa.Column(sa.String, nullable=False)

    def dump_to_xml(self, root_elem):
        root_elem.append(EM.subject(self.subject))


class ReleasesNotificationEventParam(models.Base):
    __tablename__ = 'releases_notification_event_param'
    __table_args__ = {'schema': 'service'}

    param_id = sa.Column(sa.BigInteger, primary_key=True)
    task_id = sa.Column(sa.BigInteger,
                        sa.ForeignKey(ReleasesNotificationTask.id))
    subject = sa.Column(sa.String, nullable=False)
    event_name = sa.Column(sa.String, nullable=False)

    def dump_to_xml(self, root_elem):
        root_elem.append(EM.subject(self.subject))
        root_elem.append(EM.event_name(self.event_name))
