from Products.CMFCore.utils import getToolByName
from Products.CMFPlone.PloneBatch import Batch
from Products.Five import BrowserView
from Products.statusmessages.interfaces import IStatusMessage
from ZODB.POSException import ConflictError
from ftw.publisher.core import states
from ftw.publisher.sender import message_factory as _
from ftw.publisher.sender.browser.interfaces import IEditRealmSchema
from ftw.publisher.sender.browser.interfaces import IRealmSchema
from ftw.publisher.sender.interfaces import IConfig
from ftw.publisher.sender.interfaces import IQueue
from ftw.publisher.sender.persistence import Realm
from ftw.publisher.sender.utils import sendRequestToRealm
from ftw.table.interfaces import ITableGenerator
from persistent.dict import PersistentDict
from persistent.list import PersistentList
from plone.z3cform import z2
from plone.z3cform.interfaces import IWrappedForm
from z3c.form import button
from z3c.form import field
from z3c.form import form
from z3c.form import interfaces
from zope.component import getUtility
from zope.interface import implements
from zope.publisher.interfaces import Retry
import datetime
import md5
 
 
EXECUTED_JOBS_BATCH_SIZE = 100
 
 
# lets translate the actions with i18ndude
TRANSLATED_ACTIONS = {
    'push': _(u'action_push', default=u'Push'),
    'move': _(u'action_move', default=u'Move'),
    'delete': _(u'action_delete', default=u'Delete'),
    }
 
 
# -- Forms
 
class CreateRealmForm(form.Form):
    """
    The CreateRealmForm is a z3c-form used for adding a new Realm
    instance to the publisher configuration.
 
    @cvar fields:           fields from the schema IRealmSchema
    @cvar ignoreContext:    do not use context (z3c-form setting)
    @cvar label:            label of the form
    """
    implements(IWrappedForm)
 
    fields = field.Fields(IRealmSchema)
    ignoreContext = True
    label = u'Add Realm'
 
    @button.buttonAndHandler(_(u'button_save_realm', default=u'Save Realm'))
    def handleAdd(self, action):
        """
        This handler handles a click on the "Add Realm"-Button.
        If no errors occured, it adds a new Realm to the Config.
        @param action:      ActionInfo object provided by z3c.form
        @return:            None (form is shown) or Response-redirect
        """
        data, errors = self.extractData()
        config = IConfig(self.context)
        if len(errors)==0:
            assert config.is_update_realms_possible()
            # url + username has to be unique
            for realm in config.getRealms():
                if realm.url==data['url'] and realm.username==data['username']:
                    self.statusMessage(
                        'This URL / Username combination already exists!',
                        'error')
                    return
            kwargs = {
                'active': data['active'] and 1 or 0,
                'url': data['url'],
                'username': data['username'],
                'password': data['password'],
                }
            realm = Realm(**kwargs)
            config.appendRealm(realm)
            self.statusMessage('Added realm successfully')
            return self.request.RESPONSE.redirect('./@@publisher-config')
 
    def statusMessage(self, message, type='info'):
        """
        Adds a Plone statusMessage to the session.
        @param message:         Message to display
        @type message:          string
        @param type:            Type of the message [info|warning|error]
        @type type:             string
        @return:                None
        """
        IStatusMessage(self.request).addStatusMessage(
            message,
            type=type)
 
 
class EditRealmForm(form.EditForm):
    """
    The EditRealmForm is used for editing a Realm object.
 
    @cvar fields:           fields from the schema IRealmSchema
    @cvar ignoreContext:    do not use context (z3c-form setting)
    @cvar label:            label of the form
    """
    implements(IWrappedForm)
 
    fields = field.Fields(IEditRealmSchema)
    ignoreContext = True
    label = u'Edit Realm'
 
    def updateWidgets(self):
        """
        Updates the widgets (z3c-form method).
        Customized for adding a HIDDEN_MODE-Flag to the ID-field.
        """
        super(EditRealmForm, self).updateWidgets()
        self.widgets['id'].mode = interfaces.HIDDEN_MODE
 
    @button.buttonAndHandler(_(u'button_save_realm', default=u'Save Realm'))
    def handleSave(self, action):
        """
        """
        data, errors = self.extractData()
        config = IConfig(self.context)
        assert config.is_update_realms_possible()
        if len(errors)==0:
            # get realm
            currentRealm = self.getRealmById(data['id'])
            if not currentRealm:
                raise Exception('Could not find realm')
            # no other realm should have same url+username
            for realm in config.getRealms():
                if realm!=currentRealm:
                    if realm.username==data['username'] and\
                       realm.url==data['url']:
                        self.statusMessage(
                            'This URL / Username combination already exists!',
                            )
                        return
            # update realm
            currentRealm.active = data['active'] and 1 or 0
            currentRealm.url = data['url']
            currentRealm.username = data['username']
            if data['password'] and len(data['password'])>0:
                currentRealm.password = data['password']
            self.statusMessage('Updated realm successfully')
            return self.request.RESPONSE.redirect('./@@publisher-config')
 
    def statusMessage(self, message, type='info'):
        IStatusMessage(self.request).addStatusMessage(
            message,
            type=type)
 
    def makeRealmId(self, realm):
        return md5.md5('%s-%s' % (realm.url, realm.username)).hexdigest()
 
    def getRealmById(self, id):
        for realm in IConfig(self.context).getRealms():
            if self.makeRealmId(realm)==id:
                return realm
        return None
 
 
# -- Views
 
class PublisherConfigletView(BrowserView):
 
    def __init__(self, *args, **kwargs):
        super(PublisherConfigletView, self).__init__(*args, **kwargs)
        self.config = IConfig(self.context)
        self.queue = IQueue(self.context)
 
    def makeRealmId(self, realm):
        return md5.md5('%s-%s' % (realm.url, realm.username)).hexdigest()
 
    def getRealmById(self, id):
        for realm in self.config.getRealms():
            if self.makeRealmId(realm)==id:
                return realm
        return None
 
    def statusMessage(self, message, type='info'):
        IStatusMessage(self.request).addStatusMessage(
            message,
            type=type)
 
 
class ConfigView(PublisherConfigletView):
 
    def __call__(self, *args, **kwargs):
        redirect = False
        if self.request.get('enable-publishing'):
            self.config.set_publishing_enabled(True)
            redirect = True
        elif self.request.get('disable-publishing'):
            self.config.set_publishing_enabled(False)
            redirect = True
        elif self.request.get('submit_ignored_fields'):
            ignore = {}
            add_ignore_id = self.request.get('add_ignore_id', None)
            add_ignore_fields = self.request.get('add_ignore_fields', None)
            #override saved ignores
            for ign_id in self.request.get('ign_ids', []):
                ign = self.request.get(ign_id, '')
                if ign.replace('\r\n', '').strip():
                    ignore[ign_id] = PersistentList(
                        [term.strip() for term in ign.split('\r\n') if term])
            # add new ignores
            if add_ignore_id and add_ignore_fields:
                fields = add_ignore_fields.split('\r\n')
                ignore[add_ignore_id] = PersistentList(
                    [term.strip() for term in fields if term])
 
            self.config.set_ignored_fields(PersistentDict(ignore))
            redirect = True
 
        #set locking flag
        if 'enable-locking' in self.request:
            self.config.set_locking_enabled(self.request.get('enable-locking'))
            redirect = True
 
 
        if redirect:
            return self.request.RESPONSE.redirect('./@@publisher-config')
        return super(ConfigView, self).__call__(*args, **kwargs)
 
    def getTypesInformation(self):
        """ Gets the ignored types form annotations.
            Returns the types with ignored fields and in second position
            all other types.
        """
        types_tool = getToolByName(self.context, 'portal_types')
        portal_types = types_tool.listTypeInfo()
        ignored_types = self.config.get_ignored_fields()
        portal_types = [ptype for ptype in portal_types \
                              if ptype.id not in ignored_types.keys()]
        return [ignored_types, portal_types]
 
    def getQueueSize(self):
        return IQueue(self.context).countJobs()
 
    def getRealms(self):
        """
        Returns a list of realms prepared for the template
        Example: [
        {
        'id' : '7815696ecbf1c96e6894b779456d330e',
        'active' : True,
        'url' : 'http://localhost:8080/targetSite/',
        'username' : 'blubb',
        'odd' : True,
        },
        {
        'id' : 'a67995ad3ec084cb38d32725fd73d9a3',
        'active' : False,
        'url' : 'http://localhost:8080/targetSite/',
        'username' : 'bla',
        'odd' : False,
        }
        ]
        """
        realmlist = []
        for i, realm in enumerate(self.config.getRealms()):
            id = self.makeRealmId(realm)
            realmlist.append({
                    'id': id,
                    'active': realm.active,
                    'url': realm.url,
                    'username': realm.username,
                    'odd': not ((i/2)*2==i),
                    })
        return realmlist
 
    def get_cache_folder_path(self):
        return self.config.getDataFolder()
 
    def get_clear_confirm_message(self):
        """Translated confirm message for clearing the queue
        """
        return self.context.translate(_(
                u'confirm_clear_queue',
                default=u'Are you sure to delete all jobs in the queue?'))
 
 
class ListJobs(PublisherConfigletView):
 
    def getJobs(self):
        """
        Returns all jobs that are currently in the queue.
        """
        return self.queue.getJobs()
 
 
class ListExecutedJobs(PublisherConfigletView):
 
    COLUMNS = (('date', _(u'th_date', default=u'Date')),
               ('title', _(u'th_title', default=u'Title')),
               ('action', _(u'th_action', default=u'Action')),
               ('state', _(u'th_state', default=u'State')),
               ('username', _(u'th_username', default=u'Username')),
               ('', ''))
 
    def __call__(self, *args, **kwargs):
 
        redirect = False
        if self.request.get('button.cleanup'):
            self.queue.clear_executed_jobs()
            redirect = True
 
        if self.request.get('button.delete.olderthan'):
            days = int(self.request.get('days'))
            date = datetime.datetime.now() - datetime.timedelta(days)
            self.queue.remove_executed_jobs_older_than(date)
            redirect = True
 
        requeueJob = self.request.get('requeue.job')
        if requeueJob:
            key = int(requeueJob)
            try:
                job = self.queue.get_executed_job_by_key(key)
            except KeyError:
                # could not find job
                pass
            else:
                self.queue.remove_executed_job(key)
                job.move_jsonfile_to(self.config.getDataFolder())
                self.queue.appendJob(job)
                redirect = True
 
        if redirect:
            url = './@@publisher-config-listExecutedJobs'
            return self.request.RESPONSE.redirect(url)
 
        # BATCH
        # create a fake iterable object with the length of all objects,
        # but we dont want to load them all..
        fake_data = xrange(self.queue.get_executed_jobs_length())
        b_start = int(self.request.get('b_start', 0))
        self.batch = Batch(fake_data, EXECUTED_JOBS_BATCH_SIZE, b_start)
 
        return super(ListExecutedJobs, self).__call__(*args, **kwargs)
 
    def render_table(self):
        generator = getUtility(ITableGenerator, 'ftw.tablegenerator')
        columns = [c[1] for c in ListExecutedJobs.COLUMNS]
        return generator.generate(self._get_data(), columns)
 
    def _get_data(self):
        columns = dict(ListExecutedJobs.COLUMNS)
        i18n_details = self.context.translate(_(
                u'link_job_details',
                default=u'Details'))
        i18n_requeu = self.context.translate(_(
                u'link_requeue_job',
                default='Requeue'))
        # get a batched part of the executed jobs. But we need to start
        # batching at the end, get the batch forward and then reverse,
        # because we want the newest job at the top.
        b_start = int(self.request.get('b_start', 0))
        jobs_length = self.queue.get_executed_jobs_length()
        end = jobs_length - b_start
        start = end - EXECUTED_JOBS_BATCH_SIZE
        if start < 0:
            start = 0
        entries = list(self.queue.get_executed_jobs(start, end))
        entries.reverse()
        for key, job in entries:
            state = job.get_latest_executed_entry()
            state_name = getattr(state, 'localized_name', None)
            if state_name:
                state_name = self.context.translate(state_name)
            else:
                state_name = state.__class__.__name__
            if isinstance(state, states.ErrorState):
                colored_state = '<span class="error" style="color:red;">' +\
                    '%s</span>' % self.context.translate(state_name)
            elif isinstance(state, states.WarningState):
                colored_state = '<span class="error" style="color:orange;">' +\
                    '%s</span>' % self.context.translate(state_name)
            else:
                colored_state = '<span class="success">%s</span>' % state_name
            date = 'unknown'
            try:
                date = job.executed_list[-1]['date'].strftime('%d.%m.%Y %H:%M')
            except (ConflictError, Retry):
                raise
            except:
                pass
            ctrl = ' '.join((
                    '<a href="./@@publisher-config-executed-job-details' +\
                        '?job=%s">%s</a>' % (key, i18n_details),
                    '|',
                    '<a href="./@@publisher-config-listExecutedJobs' +\
                        '?requeue.job=%s">%s</a>' % (key, i18n_requeu),
                    ))
            shortened_title = job.objectTitle
            maximum_length = 35
 
            if len(shortened_title) > maximum_length:
                try:
                    shortened_title = shortened_title.decode('utf8')
                    shortened_title = shortened_title[:maximum_length] + \
                                      u' ...'
                    shortened_title = shortened_title.encode('utf8')
                except (ConflictError, Retry):
                    raise
                except:
                    pass
            yield {
                columns['date']: date,
                columns['title']: '<a href="%s" title="%s">%s</a>' % (
                    job.objectPath + '/view',
                    job.objectTitle,
                    shortened_title),
                columns['action']: TRANSLATED_ACTIONS.get(job.action,
                                                          job.action),
                columns['state']: colored_state,
                columns['username']: job.username,
                columns['']: ctrl,
                }
 
    def get_translated_cleanup_prompt(self):
        """Returns the converted prompt string which is displayed when
        clicking on "cleanup".
 
        """
        return self.context.translate(_(
                u'prompt_cleanup',
                default=u'Are you shure to delete all executed jobs?'))
 
 
class ExecutedJobDetails(PublisherConfigletView):
 
    def __call__(self, *args, **kwargs):
        redirect_to = None
 
        self.key = int(self.request.get('job'))
        self.job = self.queue.get_executed_job_by_key(self.key)
 
        if self.request.get('button.requeue'):
            if self.job.json_file_exists():
                self.queue.remove_executed_job(self.key)
                self.job.move_jsonfile_to(self.config.getDataFolder())
                self.queue.appendJob(self.job)
                msg = _(u'info_requeued_job',
                        default=u'The job has been moved to the queue.')
                IStatusMessage(self.request).addStatusMessage(msg,
                                                              type='info')
                redirect_to = './@@publisher-config'
            else:
                msg = _(u'error_job_data_file_missing',
                        default=u'The data file of the job is missing.')
                IStatusMessage(self.request).addStatusMessage(msg,
                                                              type='error')
 
        if self.request.get('button.delete'):
            self.queue.remove_executed_job(self.key)
            if self.job.json_file_exists():
                self.job.removeJob()
            msg = _(u'info_job_deleted',
                    default=u'The job has been deleted.')
            IStatusMessage(self.request).addStatusMessage(msg,
                                                          type='info')
            redirect_to = './@@publisher-config-listExecutedJobs'
 
        if self.request.get('button.execute'):
            if self.job.json_file_exists():
                portal = self.context.portal_url.getPortalObject()
                execview = portal.restrictedTraverse(
                    '@@publisher.executeQueue')
                execview.execute_single_job(self.job)
                msg = _(u'info_job_executed',
                        default=u'The job has been executed.')
                IStatusMessage(self.request).addStatusMessage(msg,
                                                              type='info')
            else:
                msg = _(u'error_job_data_file_missing',
                        default=u'The data file of the job is missing.')
                IStatusMessage(self.request).addStatusMessage(msg,
                                                              type='error')
            redirect_to = './@@publisher-config-executed-job-details?job=' + \
                str(self.key)
 
        if redirect_to:
            return self.request.RESPONSE.redirect(redirect_to)
 
        return super(ExecutedJobDetails, self).__call__(*args, **kwargs)
 
    def get_translated_state_name(self, state):
        name = getattr(state, 'localized_name', None)
        if name:
            return self.context.translate(name)
        else:
            return state.__class__.__name__
 
    def get_translated_action(self):
        return TRANSLATED_ACTIONS.get(self.job.action, self.job.action)
 
 
class CleanJobs(PublisherConfigletView):
 
    def __call__(self, *args, **kwargs):
        self.queue._setJobs(PersistentList())
        self.statusMessage('Removed all jobs from queue')
        return self.request.RESPONSE.redirect('./@@publisher-config')
 
 
class ExecuteJobs(PublisherConfigletView):
 
    def __call__(self, *args, **kwargs):
        self.output = self.context.restrictedTraverse(
            'publisher.executeQueue')()
        self.output = self.output.replace('\n', '<br/>')
        return super(ExecuteJobs, self).__call__(self, *args, **kwargs)
 
 
class ExecuteJob(PublisherConfigletView):
 
    def __call__(self, *args, **kwargs):
        job = self.get_job()
        if not job:
            raise Exception('No job found')
        portal = self.context.portal_url.getPortalObject()
        execview = portal.restrictedTraverse('@@publisher.executeQueue')
        key = execview.execute_single_job(job)
        redirect_to = './@@publisher-config-executed-job-details?job=' + \
            str(key)
        return self.request.RESPONSE.redirect(redirect_to)
 
    def get_job(self):
        job_filename = self.request.get('job')
        for job in self.queue.getJobs():
            if job.get_filename() == job_filename:
                return job
 
 
class RemoveJob(PublisherConfigletView):
 
    def __call__(self, *args, **kwargs):
        job = self.get_job()
        if not job:
            raise Exception('No job found')
        self.queue.removeJob(job)
        job.removeJob()
        redirect_to = './@@publisher-config-listJobs'
        return self.request.RESPONSE.redirect(redirect_to)
 
    def get_job(self):
        job_filename = self.request.get('job')
        for job in self.queue.getJobs():
            if job.get_filename() == job_filename:
                return job
 
 
class AddRealm(PublisherConfigletView):
 
    def __call__(self, *args, **kwargs):
        assert self.config.is_update_realms_possible()
        self.form = self.renderForm()
        return super(AddRealm, self).__call__(*args, **kwargs)
 
    def renderForm(self):
        z2.switch_on(self)
        form = CreateRealmForm(self.context, self.request)
        return form()
 
 
class EditRealm(PublisherConfigletView):
 
    def __call__(self, *args, **kwargs):
        assert self.config.is_update_realms_possible()
        # set object values
        id = self.request.get('form.widgets.id', '')
        if id and isinstance(id, str):
            self.request.set('form.widgets.id', id.decode('utf8'))
        realm = self.getRealmById(id)
        if not realm:
            raise Exception('Could not find realm')
        values = realm.__dict__.copy()
        values['active'] = values['active'] and ['selected'] or []
 
        for k, v in values.items():
            key = 'form.widgets.%s' % k
            if key not in self.request.keys():
                if isinstance(v, str):
                    v = v.decode('utf8')
                self.request.set(key, v)
        self.form = self.renderForm()
        return super(PublisherConfigletView, self).__call__(*args, **kwargs)
 
    def renderForm(self):
        z2.switch_on(self)
        form = EditRealmForm(self.context, self.request)
        return form()
 
 
class DeleteRealm(PublisherConfigletView):
 
    def __call__(self, *args, **kwargs):
        assert self.config.is_update_realms_possible()
        id = self.request.get('id', '')
        realm = self.getRealmById(id)
        if not realm:
            self.statusMessage('Could not find realm', 'error')
        else:
            self.config.removeRealm(realm)
            self.statusMessage('Removed realm')
        return self.request.RESPONSE.redirect('./@@publisher-config')
 
 
class TestRealm(PublisherConfigletView):
 
    def __call__(self, *args, **kwargs):
        id = self.request.get('id', '')
        realm = self.getRealmById(id)
        if not realm:
            self.statusMessage(_(u'error_realm_not_found',
                                 default=u'Could not find realm'), 'error')
        else:
            responseText = sendRequestToRealm({}, realm,
                                              'publisher.testConnection')
            if responseText=='ok':
                self.statusMessage(_(u'info_realm_connection_okay',
                                     default=u'Connection okay'))
            else:
                self.statusMessage(
                    _(u'error_realm_connection_failed',
                      default=u'Connection to realm failed: ${msg}',
                      mapping=dict(msg=responseText.decode('utf-8'))),
                    type='error')
        return self.request.RESPONSE.redirect('./@@publisher-config')