import logging
from urllib import basejoin
from cStringIO import StringIO

from tg import expose, redirect, flash
from tg.decorators import without_trailing_slash
from pylons import request, app_globals as g, tmpl_context as c
from bson import ObjectId

from ming.orm import session, state
from ming.utils import LazyProperty

from allura.lib.helpers import push_config, vardec
from allura.lib.security import require, has_access, require_access
from allura import model
from allura.controllers import BaseController
from allura.lib.decorators import require_post, event_handler
from allura.lib.utils import permanent_redirect

log = logging.getLogger(__name__)

class ConfigOption(object):

    def __init__(self, name, ming_type, default):
        self.name, self.ming_type, self._default = (
            name, ming_type, default)

    @property
    def default(self):
        if callable(self._default):
            return self._default()
        return self._default

class SitemapEntry(object):

    def __init__(self, label, url=None, children=None, className=None, ui_icon=None, small=None):
        self.label = label
        self.className = className
        if url is not None:
            url = url.encode('utf-8')
        self.url = url
        self.small = small
        self.ui_icon = ui_icon
        if children is None:
            children = []
        self.children = children

    def __getitem__(self, x):
        if isinstance(x, (list, tuple)):
            self.children.extend(list(x))
        else:
            self.children.append(x)
        return self

    def __repr__(self):
        l = ['<SitemapEntry ']
        l.append('    label=%r' % self.label)
        l.append('    children=%s' % repr(self.children).replace('\n', '\n    '))
        l.append('>')
        return '\n'.join(l)

    def bind_app(self, app):
        lbl = self.label
        url = self.url
        if callable(lbl):
            lbl = lbl(app)
        if url is not None:
            url = basejoin(app.url, url)
        return SitemapEntry(lbl, url, [
                ch.bind_app(app) for ch in self.children], className=self.className)

    def extend(self, sitemap):
        child_index = dict(
            (ch.label, ch) for ch in self.children)
        for e in sitemap:
            lbl = e.label
            match = child_index.get(e.label)
            if match and match.url == e.url:
                match.extend(e.children)
            else:
                self.children.append(e)
                child_index[lbl] = e

class WidgetController(BaseController):
    widgets=[]

    def __init__(self, app): pass

    def portlet(self, content):
        return '<div class="portlet">%s</div>' % content

class Application(object):
    """
    The base Allura pluggable application

    After extending this, expose the app by adding an entry point in your setup.py:

        [allura]
        myapp = foo.bar.baz:MyAppClass

    :var status: the status level of this app.  'production' apps are available to all projects
    :var bool searchable: toggle if the search box appears in the left menu
    :var permissions: a list of named permissions used by the app
    :var sitemap: a list of :class:`SitemapEntries <allura.app.SitemapEntry>` to create an app navigation.
    :var bool installable: toggle if the app can be installed in a project
    :var bool hidden: toggle if the app should be hidden from the list of tools
    :var Controller self.root: the root Controller used for the app
    :var Controller self.api_root: a Controller used for API access at /rest/<neighborhood>/<project>/<app>/
    :var Controller self.admin: a Controller used in the admin interface
    """

    __version__ = None
    config_options = [
        ConfigOption('mount_point', str, 'app'),
        ConfigOption('mount_label', str, 'app'),
        ConfigOption('ordinal', int, '0') ]
    status_map = [ 'production', 'beta', 'alpha', 'user' ]
    status='production'
    script_name=None
    root=None  # root controller
    api_root=None
    permissions=[]
    sitemap = [ ]
    installable=True
    widget = WidgetController
    searchable = False
    DiscussionClass = model.Discussion
    PostClass = model.Post
    AttachmentClass = model.DiscussionAttachment
    tool_label='Tool'
    default_mount_label='Tool Name'
    default_mount_point='tool'
    ordinal=0
    hidden = False
    icons={
        24:'images/admin_24.png',
        32:'images/admin_32.png',
        48:'images/admin_48.png'
    }

    def __init__(self, project, app_config_object):
        self.project = project
        self.config = app_config_object
        self.admin = DefaultAdminController(self)

    @LazyProperty
    def url(self):
        return self.config.url(project=self.project)

    @property
    def acl(self):
        return self.config.acl

    def parent_security_context(self):
        return self.config.parent_security_context()

    @classmethod
    def status_int(self):
        return self.status_map.index(self.status)

    @classmethod
    def icon_url(self, size):
        '''Subclasses (tools) provide their own icons (preferred) or in
        extraordinary circumstances override this routine to provide
        the URL to an icon of the requested size specific to that tool.

        Application.icons is simply a default if no more specific icon
        is available.
        '''
        resource = self.icons.get(size)
        if resource:
            return g.theme_href(resource)
        return ''

    def has_access(self, user, topic):
        '''Whether the user has access to send email to the given topic'''
        return False

    def is_visible_to(self, user):
        '''Whether the user can view the app.'''
        return has_access(self, 'read')(user=user)

    def subscribe_admins(self):
        for uid in g.credentials.userids_with_named_role(self.project._id, 'Admin'):
            model.Mailbox.subscribe(
                type='direct',
                user_id=uid,
                project_id=self.project._id,
                app_config_id=self.config._id)

    @classmethod
    def default_options(cls):
        ":return: the default config options"
        return dict(
            (co.name, co.default)
            for co in cls.config_options)

    def install(self, project):
        'Whatever logic is required to initially set up a tool'
        # Create the discussion object
        discussion = self.DiscussionClass(
            shortname=self.config.options.mount_point,
            name='%s Discussion' % self.config.options.mount_point,
            description='Forum for %s comments' % self.config.options.mount_point)
        session(discussion).flush()
        self.config.discussion_id = discussion._id
        self.subscribe_admins()

    def uninstall(self, project=None, project_id=None):
        'Whatever logic is required to tear down a tool'
        if project_id is None: project_id = project._id
        # De-index all the artifacts belonging to this tool in one fell swoop
        g.solr.delete(q='project_id_s:"%s" AND mount_point_s:"%s"' % (
                project_id, self.config.options['mount_point']))
        for d in model.Discussion.query.find({
                'project_id':project_id,
                'app_config_id':self.config._id}):
            d.delete()
        self.config.delete()
        session(self.config).flush()

    def main_menu(self):
        '''Apps should provide their entries to be added to the main nav
        :return: a list of :class:`SitemapEntries <allura.app.SitemapEntry>`
        '''
        return self.sitemap

    def sidebar_menu(self):
        """
        Apps should override this to provide their menu
        :return: a list of :class:`SitemapEntries <allura.app.SitemapEntry>`
        """
        return []

    def sidebar_menu_js(self):
        """
        Apps can override this to provide Javascript needed by the sidebar_menu.
        :return: a string of Javascript code
        """
        return ""

    def admin_menu(self, force_options=False):
        """
        Apps may override this to provide additional admin menu items
        :return: a list of :class:`SitemapEntries <allura.app.SitemapEntry>`
        """
        admin_url = c.project.url()+'admin/'+self.config.options.mount_point+'/'
        links = []
        if self.permissions and has_access(c.project, 'admin')():
            links.append(SitemapEntry('Permissions', admin_url + 'permissions'))
        if force_options or len(self.config_options) > 3:
            links.append(SitemapEntry('Options', admin_url + 'options', className='admin_modal'))
        links.append(SitemapEntry('Label', admin_url + 'edit_label', className='admin_modal'))
        return links

    def handle_message(self, topic, message):
        '''Handle incoming email msgs addressed to this tool'''
        pass

    def handle_artifact_message(self, artifact, message):
        # Find ancestor comment and thread
        thd, parent_id = artifact.get_discussion_thread(message)
        # Handle attachments
        message_id = message['message_id']
        if message.get('filename'):
            # Special case - the actual post may not have been created yet
            log.info('Saving attachment %s', message['filename'])
            fp = StringIO(message['payload'])
            self.AttachmentClass.save_attachment(
                message['filename'], fp,
                content_type=message.get('content_type', 'application/octet-stream'),
                discussion_id=thd.discussion_id,
                thread_id=thd._id,
                post_id=message_id,
                artifact_id=message_id)
            return
        # Handle duplicates
        post = self.PostClass.query.get(_id=message_id)
        if post:
            log.info('Existing message_id %s found - saving this as text attachment' % message_id)
            fp = StringIO(message['payload'])
            post.attach(
                'alternate', fp,
                content_type=message.get('content_type', 'application/octet-stream'),
                discussion_id=thd.discussion_id,
                thread_id=thd._id,
                post_id=message_id)
        else:
            text=message['payload'] or '--no text body--'
            post = thd.post(
                message_id=message_id,
                parent_id=parent_id,
                text=text,
                subject=message['headers'].get('Subject', 'no subject'))

class DefaultAdminController(BaseController):

    def __init__(self, app):
        self.app = app

    @expose()
    def index(self, **kw):
        permanent_redirect('permissions')

    @expose('jinja:allura:templates/app_admin_permissions.html')
    @without_trailing_slash
    def permissions(self):
        from ext.admin.widgets import PermissionCard
        c.card = PermissionCard()
        permissions = dict((p, []) for p in self.app.permissions)
        for ace in self.app.config.acl:
            if ace.access == model.ACE.ALLOW:
                try:
                    permissions[ace.permission].append(ace.role_id)
                except KeyError:
                    # old, unknown permission
                    pass
        return dict(
            app=self.app,
            allow_config=has_access(c.project, 'admin')(),
            permissions=permissions)

    @expose('jinja:allura:templates/app_admin_edit_label.html')
    def edit_label(self):
        return dict(
            app=self.app,
            allow_config=has_access(self.app, 'configure')())

    @expose()
    @require_post()
    def update_label(self, mount_label):
        require_access(self.app, 'configure')
        self.app.config.options['mount_label'] = mount_label
        redirect(request.referer)

    @expose('jinja:allura:templates/app_admin_options.html')
    def options(self):
        return dict(
            app=self.app,
            allow_config=has_access(self.app, 'configure')())

    @expose()
    @require_post()
    def configure(self, **kw):
        with push_config(c, app=self.app):
            require_access(self.app, 'configure')
            is_admin = self.app.config.tool_name == 'admin'
            if kw.pop('delete', False):
                if is_admin:
                    flash('Cannot delete the admin tool, sorry....')
                    redirect('.')
                c.project.uninstall_app(self.app.config.options.mount_point)
                redirect('..')
            for k,v in kw.iteritems():
                self.app.config.options[k] = v
            if is_admin:
                # possibly moving admin mount point
                redirect('/'
                         + c.project._id
                         + self.app.config.options.mount_point
                         + '/'
                         + self.app.config.options.mount_point
                         + '/')
            else:
                redirect(request.referer)

    @without_trailing_slash
    @expose()
    @vardec
    @require_post()
    def update(self, card=None, **kw):
        self.app.config.acl = []
        for args in card:
            perm = args['id']
            new_group_ids = args.get('new', [])
            group_ids = args.get('value', [])
            if isinstance(new_group_ids, basestring):
                new_group_ids = [ new_group_ids ]
            if isinstance(group_ids, basestring):
                group_ids = [ group_ids ]
            role_ids = map(ObjectId, group_ids + new_group_ids)
            self.app.config.acl += [
                model.ACE.allow(r, perm) for r in role_ids]
        redirect(request.referer)

@event_handler('project_updated')
def subscribe_admins(topic):
    for ac in c.project.app_configs:
        c.project.app_instance(ac).subscribe_admins()
