"""Cornice API for ideas"""
from collections import defaultdict
from datetime import datetime
from functools import partial
import simplejson as json
from cornice import Service
from pyramid.httpexceptions import (
    HTTPNotFound, HTTPBadRequest, HTTPNoContent, HTTPUnauthorized)
from pyramid.security import authenticated_userid, Everyone
from sqlalchemy import and_
from sqlalchemy.orm import (joinedload, subqueryload, undefer)
from assembl.lib.parsedatetime import parse_datetime
from assembl.models import (
    Idea, RootIdea, IdeaLink, Discussion, IdeaExtractLink,
    Extract, SubGraphIdeaAssociation, LangString)
from assembl.auth import (
    CrudPermissions, P_READ, P_ADD_IDEA, P_EDIT_IDEA, P_READ_IDEA,
    P_ASSOCIATE_IDEA)
from . import (
    API_DISCUSSION_PREFIX, instance_check_op, instance_check_permission,
    instance_check_permission_id)
ideas = Service(name='ideas', path=API_DISCUSSION_PREFIX + '/ideas',
                description="The ideas collection",
                renderer='json')
idea = Service(name='idea', path=API_DISCUSSION_PREFIX + '/ideas/{id:.+}',
               description="Manipulate a single idea", renderer='json')
idea_extracts = Service(
    name='idea_extracts',
    path=API_DISCUSSION_PREFIX + '/ideas_extracts/{id:.+}',
    description="Get the extracts of a single idea")
langstring_fields = {
    "longTitle": "synthesis_title",
    "shortTitle": "title",
    "definition": "description"
}
def idea_check_permission(request, permission=P_READ_IDEA, **kwargs):
    return instance_check_permission(request, permission, Idea)
def idea_check_op(request, op=CrudPermissions.READ, **kwargs):
    return instance_check_op(request, op, Idea)
def check_add_on_parent(request, **kwargs):
    idea_data = request.json_body or {}
    parent_id = idea_data.get('parentId', None)
    if parent_id:
        return instance_check_permission_id(
            request, P_ADD_IDEA, Idea, parent_id)
    elif P_ADD_IDEA in request.base_permissions:
        return True
    else:
        request.errors.add("querystring", 'permissions', "Cannot add idea")
        request.errors.status = 403
        return False
    return True
# Create
@ideas.post(validators=check_add_on_parent)
def create_idea(request):
    discussion = request.context
    session = discussion.db
    user_id = authenticated_userid(request)
    permissions = request.permissions
    idea_data = request.json_body
    now = datetime.utcnow()
    pub_state = None
    pub_flow = discussion.idea_publication_flow
    if pub_flow:
        pub_state_name = discussion.preferences['default_idea_pub_state']
        pub_state = pub_flow.state_by_label(pub_state_name)
    kwargs = {
        "discussion": discussion,
        "creation_date": now,
        "pub_state": pub_state,
        "creator_id": user_id,
    }
    new_idea = Idea(**kwargs)
    session.add(new_idea)
    context = new_idea.get_instance_context(request=request)
    for key, attr_name in langstring_fields.items():
        if key in idea_data:
            ls_data = idea_data[key]
            if ls_data is None:
                continue
            subcontext = new_idea.get_collection_context(key, context)
            current = LangString.create_from_json(
                ls_data, context=subcontext)
            setattr(new_idea, attr_name, current._instance)
    if idea_data['parentId']:
        parent = Idea.get_instance(idea_data['parentId'])
    else:
        parent = discussion.root_idea
    session.add(IdeaLink(
        source=parent, target=new_idea, creation_date=now,
        order=idea_data.get('order', 0.0)))
    session.flush()
    return {'ok': True, '@id': new_idea.uri()}
@idea.get(validators=idea_check_op)
def get_idea(request):
    idea_id = request.matchdict['id']
    idea = Idea.get_instance(idea_id)
    view_def = request.GET.get('view')
    discussion = request.context
    user_id = authenticated_userid(request) or Everyone
    permissions = request.permissions
    if not idea:
        raise HTTPNotFound("Idea with id '%s' not found." % idea_id)
    return idea.generic_json(view_def, user_id, permissions)
def _get_ideas_real(request, view_def=None, ids=None, user_id=None,
                    modified_after=None):
    discussion = request.discussion
    user_id = user_id or Everyone
    # optimization: Recursive widget links.
    from assembl.models import (
        Widget, IdeaWidgetLink, IdeaDescendantsShowingWidgetLink)
    universal_widget_links = []
    by_idea_widget_links = defaultdict(list)
    widget_links = discussion.db.query(IdeaWidgetLink
        ).join(Widget).join(Discussion
        ).filter(
            Widget.test_active(),
            Discussion.id == discussion.id,
            IdeaDescendantsShowingWidgetLink.polymorphic_filter()
        ).options(joinedload(IdeaWidgetLink.idea)).all()
    for wlink in widget_links:
        if isinstance(wlink.idea, RootIdea):
            universal_widget_links.append({
                '@type': wlink.external_typename(),
                'widget': Widget.uri_generic(wlink.widget_id)})
        else:
            for id in wlink.idea.get_all_descendants(True):
                by_idea_widget_links[Idea.uri_generic(id)].append({
                    '@type': wlink.external_typename(),
                    'widget': Widget.uri_generic(wlink.widget_id)})
    next_synthesis_id = discussion.get_next_synthesis_id()
    ideas = Idea.query_filter_with_crud_op_req(request).filter(
        Idea.discussion==discussion)
    if modified_after:
        ideas = ideas.filter(Idea.last_modified > modified_after)
    ideas = ideas.outerjoin(
        SubGraphIdeaAssociation,
        and_(SubGraphIdeaAssociation.sub_graph_id == next_synthesis_id,
             SubGraphIdeaAssociation.idea_id == Idea.id))
    ideas = ideas.outerjoin(
        IdeaLink, IdeaLink.target_id == Idea.id)
    ideas = ideas.order_by(IdeaLink.order, Idea.creation_date)
    if ids:
        ids = [Idea.get_database_id(id) for id in ids]
        ideas = ideas.filter(Idea.id.in_(ids))
    # remove tombstones
    ideas = ideas.filter(and_(*Idea.base_conditions()))
    ideas = ideas.options(
        joinedload(Idea.source_links),
        subqueryload(Idea.attachments).joinedload("document"),
        subqueryload(Idea.widget_links),
        joinedload(Idea.title).subqueryload("entries"),
        joinedload(Idea.synthesis_title).subqueryload("entries"),
        joinedload(Idea.description).subqueryload("entries"),
        joinedload(Idea.import_record),
        undefer(Idea.num_children))
    permissions = request.permissions
    Idea.prepare_counters(discussion.id, True)
    # ideas = list(ideas)
    # import cProfile
    # cProfile.runctx('''retval = [idea.generic_json(None, %d, %s)
    #           for idea in ideas]''' % (user_id, permissions),
    #           globals(), locals(), 'json_stats')
    retval = [idea.generic_json(view_def, user_id, permissions)
              for idea in ideas]
    retval = [x for x in retval if x is not None]
    for r in retval:
        if r.get('widget_links', None) is not None:
            links = r['widget_links'][:]
            links.extend(universal_widget_links)
            links.extend(by_idea_widget_links[r['@id']])
            r['active_widget_links'] = links
    return retval
@ideas.get(permission=P_READ)
def get_ideas(request):
    user_id = authenticated_userid(request) or Everyone
    discussion = request.context
    view_def = request.GET.get('view')
    ids = request.GET.getall('ids')
    modified_after = request.GET.get('modified_after')
    if modified_after:
        modified_after = parse_datetime(modified_after, True)
    return _get_ideas_real(
        request, view_def=view_def, ids=ids, user_id=user_id,
        modified_after=modified_after)
[docs]@idea.put(validators=partial(idea_check_op, op=CrudPermissions.UPDATE))
def save_idea(request):
    """Update this idea.
    In case the ``parentId`` is changed, handle all
    ``IdeaLink`` changes and send relevant ideas on the socket."""
    discussion = request.context
    user_id = authenticated_userid(request)
    permissions = request.permissions
    idea_id = request.matchdict['id']
    idea_data = json.loads(request.body)
    # Idea.default_db.execute('set transaction isolation level read committed')
    # Special items in TOC, like unsorted posts.
    if idea_id in ['orphan_posts']:
        return {'ok': False, 'id': Idea.uri_generic(idea_id)}
    idea = Idea.get_instance(idea_id)
    db = idea.db
    if not idea:
        raise HTTPNotFound("No such idea: %s" % (idea_id))
    if isinstance(idea, RootIdea):
        raise HTTPBadRequest("Cannot edit root idea.")
    if(idea.discussion_id != discussion.id):
        raise HTTPBadRequest(
            "Idea from discussion %s cannot be saved from different discussion (%s)." % (
                idea.discussion_id, discussion.id))
    context = idea.get_instance_context(request=request)
    for key, attr_name in langstring_fields.items():
        if key in idea_data:
            current = getattr(idea, attr_name)
            ls_data = idea_data[key]
            # TODO: handle legacy string instance?
            if isinstance(ls_data, str):
                tr_service = discussion.translation_service()
                locale = tr_service.identify(ls_data)[0]
                ls_data = {
                    '@type': 'LangString',
                    'entries': [{
                        '@type': 'LangStringEntry',
                        '@language': locale,
                        'value': ls_data}]}
            elif isinstance(ls_data, dict):
                for e in ls_data['entries']:
                    if e['@language'] == 'und':
                        e['@language'] = tr_service.identify(e['value'])[0]
            subcontext = idea.get_collection_context(key, context)
            if current:
                if ls_data:
                    current.update_from_json(
                        ls_data, context=subcontext, permissions=permissions)
                else:
                    current.delete()
            elif ls_data:
                current = LangString.create_from_json(ls_data, context=subcontext)
                setattr(idea, attr_name, current._instance)
    new_parent_id = idea_data.get('parentId', None)
    if new_parent_id:
        # TODO: Make sure this is sent as a list!
        # Actually, use embedded links to do this properly...
        new_parent_ids = {new_parent_id}
        old_parent_ids = {Idea.uri_generic(l.source_id) for l in idea.source_links}
        if new_parent_ids != old_parent_ids:
            added_parent_ids = new_parent_ids - old_parent_ids
            removed_parent_ids = old_parent_ids - new_parent_ids
            added_parents = [Idea.get_instance(id) for id in added_parent_ids]
            current_parents = idea.get_parents()
            removed_parents = [p for p in current_parents 
                if p.uri() in removed_parent_ids]
            if None in added_parents:
                missing = [id for id in added_parent_ids if not Idea.get_instance(id)]
                raise HTTPNotFound("Missing parentId %s" % (','.join(missing)))
            if not idea.has_permission_req(P_ASSOCIATE_IDEA):
                raise HTTPUnauthorized("Cannot associate idea "+idea.uri())
            for parent in added_parents + removed_parents:
                if not parent.has_permission_req(P_ASSOCIATE_IDEA):
                    raise HTTPUnauthorized("Cannot associate parent idea "+idea.uri())
            old_ancestors = set()
            new_ancestors = set()
            for parent in current_parents:
                old_ancestors.add(parent)
                old_ancestors.update(parent.get_all_ancestors())
            kill_links = {l for l in idea.source_links
                if Idea.uri_generic(l.source_id) in removed_parent_ids}
            order = idea_data.get('order', 0.0)
            for parent in added_parents:
                if kill_links:
                    link = kill_links.pop()
                    db.expire(link.source, ['target_links'])
                    link.copy(True)
                    link.order = order
                    link.source = parent
                else:
                    link = IdeaLink(source=parent, target=idea, order=order)
                    db.add(link)
                db.expire(parent, ['target_links'])
                order += 1.0
            for link in kill_links:
                db.expire(link.source, ['target_links'])
                kill_links.is_tombstone = True
            db.expire(idea, ['source_links'])
            db.flush()
            for parent in idea.get_parents():
                new_ancestors.add(parent)
                new_ancestors.update(parent.get_all_ancestors())
            for ancestor in new_ancestors ^ old_ancestors:
                ancestor.send_to_changes()
        else:
            order = idea_data.get('order', None)
            if order is not None:
                new_parent_id = Idea.get_database_id(new_parent_id)
                parent_links = [link for link in idea.source_links
                                if link.source_id == new_parent_id]
                assert len(parent_links) == 1
                parent_links[0].order = idea_data.get('order', 0.0)
    if 'subtype' in idea_data:
        idea.rdf_type = idea_data['subtype']
    idea.send_to_changes()
    return {'ok': True, 'id': idea.uri()} 
@idea.delete(validators=partial(idea_check_op, op=CrudPermissions.DELETE))
def delete_idea(request):
    idea_id = request.matchdict['id']
    idea = Idea.get_instance(idea_id)
    if not idea:
        raise HTTPNotFound("Idea with id '%s' not found." % idea_id)
    if isinstance(idea, RootIdea):
        raise HTTPBadRequest("Cannot delete root idea.")
    num_childrens = len(idea.children)
    if num_childrens > 0:
        raise HTTPBadRequest("Idea cannot be deleted because it still has %d child ideas." % num_childrens)
    num_extracts = len(idea.extracts)
    if num_extracts > 0:
        raise HTTPBadRequest("Idea cannot be deleted because it still has %d extracts." % num_extracts)
    for link in idea.source_links:
        link.is_tombstone = True
    idea.is_tombstone = True
    # Maybe return tombstone() ?
    request.response.status = HTTPNoContent.code
    return HTTPNoContent()
@idea_extracts.get(validators=idea_check_op)
def get_idea_extracts(request):
    discussion = request.context
    idea_id = request.matchdict['id']
    idea = Idea.get_instance(idea_id)
    view_def = request.GET.get('view') or 'default'
    user_id = authenticated_userid(request) or Everyone
    permissions = request.permissions
    if not idea:
        raise HTTPNotFound("Idea with id '%s' not found." % idea_id)
    extracts = Extract.default_db.query(Extract).join(IdeaExtractLink).filter(
        IdeaExtractLink.idea_id == idea.id
    ).order_by(Extract.order.desc())
    return [extract.generic_json(view_def, user_id, permissions)
            for extract in extracts]