# Kanbanara Search Component
# Written by Rebecca Shalfield between 2013 and 2017
# Copyright (c) Rebecca Shalfield 2013-2017
# Released under the GNU AGPL v3

'''Kanbanara's Search Component'''

import datetime
import logging
import os
import re
import urllib.parse

import cherrypy
from kanbanara import Kanbanara
from pymongo import MongoClient


class Search(Kanbanara):
    '''Kanbanara's Search Component'''

    def __init__(self):
        """Initialisation"""
        self.component = 'api'

        Kanbanara.__init__(self)

        self.current_dir = os.path.dirname(os.path.abspath(__file__))

        logging.basicConfig(level=logging.DEBUG,
                            format='%(asctime)s - %(levelname)s - %(message)s',
                            filename=os.path.join(self.current_dir, '..', 'logs', 'kanbanara.log'),
                            filemode='w'
                           )

        # Initialisation Settings
        self.mongodb_host, self.mongodb_port, self.mongodb_username, self.mongodb_password, self.mongodb_bindir = Kanbanara.read_mongodb_ini_file(self)

        # Connect to MongoDB on given host and port
        if self.mongodb_username and self.mongodb_password:
            modified_username = urllib.parse.quote_plus(self.mongodb_username)
            modified_password = urllib.parse.quote_plus(self.mongodb_password)
            connection = MongoClient(f'mongodb://{modified_username}:{modified_password}@{self.mongodb_host}:{self.mongodb_port}')
        else:
            connection = MongoClient(self.mongodb_host, self.mongodb_port)

        # Connect to 'kanbanara' database, creating if not already exists
        kanbanara_db = connection['kanbanara']

        # Connect to 'projects' collection
        self.projects_collection = kanbanara_db['projects']
        for attribute in ['project']:
            self.projects_collection.create_index(attribute, unique=True, background=True)

        # Connect to 'sessions' collection
        self.sessions_collection = kanbanara_db['sessions']
        for attribute in ['session_id']:
            self.sessions_collection.create_index(attribute, unique=True, background=True)

        for attribute in ['lastaccess']:
            self.sessions_collection.create_index(attribute, unique=False, background=True)

        # Connect to 'members' collection
        self.members_collection = kanbanara_db['members']
        for attribute in ['username']:
            # TODO - username attribute should be unique but get error when unique=true set
            self.members_collection.create_index(attribute, unique=False, background=True)

        # Connect to 'cards' collection
        self.cards_collection = kanbanara_db['cards']

    @cherrypy.expose
    def advanced_search(self):
        """Allows an advanced search to be specified"""
        Kanbanara.check_authentication('/search/advanced_search')
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        highlight_attrs_values = session_document.get('advanced_search', {})
        member_document = Kanbanara.get_member_document(self, session_document)
        if 'project' in member_document and member_document['project']:
            project = member_document['project']
        else:
            #TODO - GIVE ERROR MESSAGE TO SELECT PROJECT!
            raise cherrypy.HTTPRedirect("/kanban/index", 302)

        content = []
        content.append(Kanbanara.header(self, 'advanced_search', 'Advanced Search'))
        content.append(Kanbanara.filter_bar(self, 'advanced_search'))
        content.append(Kanbanara.menubar(self))
        content.append(self.insert_page_title_and_online_help(session_document, 'advanced_search', 'Advanced Search'))
        content.append('<div align="center">')
        content.append('<form action="/search/submit_advanced_search" method="post"><table class="admin"><tr>')
        max_per_row = 3
        for count, attribute in enumerate(self.card_attributes_datatypes.keys(), start=1):
            content.append(f'<th>{self.displayable_key(attribute)}</th>')
            content.append(f'<td><i>{self.card_attributes_datatypes[attribute]}</i></td>')
            if attribute in ['category', 'classofservice', 'coowner', 'coreviewer', 'creator',
                             'customer', 'iteration', 'owner', 'release', 'reviewer', 'state']:
                content.append(f'<td>')
                attribute_distincts = self.cards_collection.find({'project': project}).distinct(attribute)
                current_value = highlight_attrs_values.get(attribute, '')
                content.append(self.create_html_select_block(attribute, attribute_distincts,
                                                             default='',
                                                             current_value=current_value))
                content.append('</td>')
            elif attribute == 'priority':
                content.append('<td>')
                current_value = highlight_attrs_values.get(attribute, '')
                content.append(self.create_html_select_block('priority', self.priorities,
                                                             default='',
                                                             current_value=current_value,
                                                             specials=['capitalise']))
                content.append('</td>')
            elif attribute == 'severity':
                content.append('<td>')
                current_value = highlight_attrs_values.get(attribute, '')
                content.append(self.create_html_select_block('severity', self.severities, default='', 
                                                             current_value=current_value,
                                                             specials=['capitalise']))
                content.append('</td>')
            elif self.card_attributes_datatypes[attribute] == 'boolean':
                content.append(f'<td>')
                current_value = highlight_attrs_values.get(attribute, '')
                content.append(self.create_html_select_block(attribute, ['False', 'True'],
                                                             default='',
                                                             current_value=current_value))
                content.append('</td>')
            elif self.card_attributes_datatypes[attribute] == 'datetime':
                if attribute in highlight_attrs_values and highlight_attrs_values[attribute]:
                    content.append(f'<td><input class="{attribute}" type="text" name="{attribute}" value="{highlight_attrs_values[attribute]}"></td>')
                else:
                    content.append(f'<td><input class="{attribute}" type="text" name="{attribute}"></td>')
                
            elif self.card_attributes_datatypes[attribute] in ['float', 'integer']:
                if attribute in highlight_attrs_values and highlight_attrs_values[attribute]:
                    content.append(f'<td><input type="number" name="{attribute}" value="{highlight_attrs_values[attribute]}"></td>')
                else:
                    content.append(f'<td><input type="number" name="{attribute}"></td>')
                
            else:
                if attribute in highlight_attrs_values and highlight_attrs_values[attribute]:
                    content.append(f'<td><input type="text" name="{attribute}" value="{highlight_attrs_values[attribute]}"></td>')
                else:
                    content.append(f'<td><input type="text" name="{attribute}"></td>')
            
            if count % max_per_row == 0:
                content.append('</tr></tr>')
        
        content.append('</tr>')
        content.append(f'<tr><th colspan="{max_per_row * 3}"><input type="submit" value="Search"></th></tr></table></form>')
        content.append('</div>')
        content.append(Kanbanara.footer(self))
        return "".join(content)
        
    @cherrypy.expose
    def index(self):
        """Redirects you to the kanban board"""
        raise cherrypy.HTTPRedirect("/kanban", 302)
        
    @cherrypy.expose
    def submit_advanced_search(self, actualcost='', actualtime='', affectsversion='', artifacts='',
                               broadcast='', bypassreview='', category='', classofservice='',
                               comment='', coowner='', coreviewer='', creator='', crmcase='',
                               customer='', deadline='', dependsupon='', deferred='',
                               deferreduntil='', description='', difficulty='', emotion='',
                               escalation='', estimatedcost='', estimatedtime='',
                               externalhyperlink='', externalreference='', fixversion='',
                               iteration='', nextaction='', notes='', owner='', priority='',
                               question='', recurring='', release='', resolution='', reviewer='',
                               rootcauseanalysis='', rules='', severity='', startby='', state='',
                               status='', tags='', testcases='', title=''):
        """Displays the results of an advanced search"""
        username = Kanbanara.check_authentication('/kanban/index')
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = Kanbanara.get_member_document(self, session_document)
        if 'project' in member_document and member_document['project']:
            project = member_document['project']
        else:
            raise cherrypy.HTTPRedirect("/kanban/index", 302)

        content = []
        highlight_attrs_values = {}
        fields = []
        content.append(Kanbanara.header(self, 'advanced_search', 'Advanced Search'))
        content.append(Kanbanara.filter_bar(self, 'index'))
        content.append(Kanbanara.menubar(self))
        content.append('<div align="center">')
        content.append(self.insert_page_title_and_online_help(session_document, 'advanced_search',
                                                              'Advanced Search'))
        sortable_documents = []
        search_criteria = {}
        for attribute, variable in [('actualcost', actualcost),
                                    ('actualtime', actualtime),
                                    ('affectsversion', affectsversion),
                                    ('artifacts', artifacts),
                                    ('broadcast', broadcast),
                                    ('bypassreview', bypassreview),
                                    ('category', category),
                                    ('classofservice', classofservice),
                                    ('comment', comment),
                                    ('coowner', coowner),
                                    ('coreviewer', coreviewer), 
                                    ('creator', creator), 
                                    ('crmcase', crmcase), 
                                    ('customer', customer), 
                                    ('deadline', deadline), 
                                    ('dependsupon', dependsupon), 
                                    ('deferred', deferred), 
                                    ('deferreduntil', deferreduntil),
                                    ('description', description),
                                    ('difficulty', difficulty), 
                                    ('emotion', emotion), 
                                    ('escalation', escalation), 
                                    ('estimatedcost', estimatedcost), 
                                    ('estimatedtime', estimatedtime), 
                                    ('externalhyperlink', externalhyperlink), 
                                    ('externalreference', externalreference), 
                                    ('fixversion', fixversion), 
                                    ('iteration', iteration), 
                                    ('nextaction', nextaction), 
                                    ('notes', notes), 
                                    ('owner', owner), 
                                    ('priority', priority), 
                                    ('question', question), 
                                    ('recurring', recurring), 
                                    ('release', release), 
                                    ('resolution', resolution), 
                                    ('reviewer', reviewer), 
                                    ('rootcauseanalysis', rootcauseanalysis), 
                                    ('rules', rules), 
                                    ('severity', severity), 
                                    ('startby', startby), 
                                    ('state', state), 
                                    ('status', status), 
                                    ('tags', tags), 
                                    ('testcases', testcases), 
                                    ('title', title)]:
            if variable:
                highlight_attrs_values[attribute] = variable
                if self.card_attributes_datatypes[attribute] == 'boolean':
                    if variable == 'False':
                        search_criteria[attribute] = False
                    elif variable == 'True':
                        search_criteria[attribute] = True

                elif self.card_attributes_datatypes[attribute] == 'datetime':
                    try:
                        search_criteria[attribute] = self.dashed_date_to_datetime_object(variable)
                    except:
                        search_criteria[attribute] = variable

                elif self.card_attributes_datatypes[attribute] == 'float':
                    try:
                        search_criteria[attribute] = float(variable)
                    except:
                        search_criteria[attribute] = variable

                elif self.card_attributes_datatypes[attribute] == 'integer':
                    try:
                        search_criteria[attribute] = int(variable)
                    except:
                        search_criteria[attribute] = variable
                    
                else:
                    search_criteria[attribute] = re.compile('(?i)'+variable)
                
        session_document['search_mode'] = 'advanced'
        session_document['advanced_search'] = highlight_attrs_values
        self.sessions_collection.save(session_document)
                
        if not search_criteria:
            raise cherrypy.HTTPRedirect("/search/advanced_search", 302)
        else:
            search_criteria['project'] = project
            for document in self.cards_collection.find(search_criteria):
                if 'lastchanged' in document and document['lastchanged']:
                    sortable_documents.append((document['lastchanged'], '', document))
                else:
                    sortable_documents.append((0, '', document))

            if sortable_documents:
                content.append(self.display_search_results(project, highlight_attrs_values, sortable_documents))
            else:
                content.append('<h3>Sorry, nothing found!</h3>')

        content.append('</div>')
        content.append('<script type="text/javascript" src="/scripts/listview.js"></script>')
        content.append(Kanbanara.footer(self))
        return "".join(content)

    def display_search_results(self, project, highlight_attrs_values, sortable_documents):
        content = []
        try:
            sortable_documents.sort(reverse=True)
        except:
            pass

        project_document = self.projects_collection.find_one({'project': project, 'workflow': {'$exists': True}})
        workflow = project_document['workflow']

        content.append('<table><tr>')
        for step_document in workflow:
            step_name = step_document['step']
            if step_name:
                number_of_columns = 0
                metastate = ""
                for column in ['maincolumn', 'counterpartcolumn', 'buffercolumn']:
                    if column in step_document:
                        number_of_columns += 1
                        if not metastate:
                            variable_column = step_document[column]
                            state = variable_column['state']
                            if state in self.metastates_list:
                                metastate = state
                            else:
                                custom_states = project_document.get('customstates', {})
                                metastate = custom_states[state]

                if number_of_columns:
                    if number_of_columns > 1:
                        content.append(f'<th id="{metastate}step" colspan="{number_of_columns}">{step_name}</th>')
                    else:
                        content.append(f'<th id="{metastate}step">{step_name}</th>')

        content.append('</tr><tr>')
        for step_document in workflow:
            step_name = step_document['step']
            if step_name:
                if 'maincolumn' in step_document:
                    main_column = step_document['maincolumn']
                    column_name = main_column['name']
                    state = main_column['state']
                    if state in self.metastates_list:
                        metastate = state
                    else:
                        custom_states = project_document.get('customstates', {})
                        metastate = custom_states[state]

                    content.append('<th id="'+metastate+'columnheader">'+column_name+'</th>')

                if 'counterpartcolumn' in step_document:
                    counterpart_column = step_document['counterpartcolumn']
                    column_name = counterpart_column['name']
                    state = counterpart_column['state']
                    if state in self.metastates_list:
                        metastate = state
                    else:
                        custom_states = project_document.get('customstates', {})
                        metastate = custom_states[state]

                    content.append('<th id="'+metastate+'columnheader">'+column_name+'</th>')

                if 'buffercolumn' in step_document:
                    buffer_column = step_document['buffercolumn']
                    column_name = buffer_column['name']
                    state = buffer_column['state']
                    if state in self.metastates_list:
                        metastate = state
                    else:
                        custom_states = project_document.get('customstates', {})
                        metastate = custom_states[state]

                    content.append('<th id="'+metastate+'columnheader">'+column_name+'</th>')

        content.append('</tr>')

        workflow_index = project_document.get('workflow_index', {})
        condensed_column_states = workflow_index.get('condensed_column_states', [])
        for state in condensed_column_states:
            content.append('<td valign="top">')
            for (lastchanged, field, document) in sortable_documents:
                if 'state' in document and document['state'] == state:
                    content.append(self.assemble_search_kanban_card([], -1, document['_id'], highlight_attrs_values, 0))

            content.append('</td>')

        content.append('</tr></table>')
        return "".join(content)
        
    @cherrypy.expose
    def search(self, attribute="", search="", x="", y=""):
        """comment"""
        username = Kanbanara.check_authentication('/search/search')
        session_id = Kanbanara.cookie_handling(self)
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        member_document = Kanbanara.get_member_document(self, session_document)
        if 'project' in member_document and member_document['project']:
            project = member_document['project']
        else:
            raise cherrypy.HTTPRedirect("/kanban/index", 302)

        session_document['search_mode'] = 'simple'
        session_document['simple_search'] = [attribute, search]
        self.sessions_collection.save(session_document)

        if not search:
            raise cherrypy.HTTPRedirect("/kanban/index", 302)

        content = []
        highlight_attrs_values = {}
        content.append(Kanbanara.header(self, 'search', 'Search'))
        content.append(Kanbanara.filter_bar(self, 'index'))
        content.append(Kanbanara.menubar(self))

        content.append('<div align="center">')
        content.append(self.insert_page_title_and_online_help(session_document, 'search', 'Search: "<b>'+search+'</b>"'))
        sortable_documents = []

        if attribute:
            fields = [attribute]
            highlight_attrs_values[attribute] = search
        else:
            for searchable_attribute in self.searchable_attributes:
                highlight_attrs_values[searchable_attribute] = search
            
            fields = self.searchable_attributes

        required_columns, required_states = self.get_displayable_columns()

        # Search under current filter bar settings
        content.append('<h3>Searching under current filter bar settings...</h3>')
        _, _, owner_reviewer_search_criteria = self.get_filtered_search_criteria(session_document, required_states)
        for field in fields:
            owner_reviewer_search_criteria[field] = re.compile('(?i)'+search)
            for document in self.cards_collection.find(owner_reviewer_search_criteria):
                if 'lastchanged' in document and document['lastchanged']:
                    sortable_documents.append((document['lastchanged'], field, document))
                else:
                    sortable_documents.append((0, field, document))

        # Search with current filter bar settings minus release and iteration
        if not sortable_documents:
            content.append('<h3>Searching under current filter bar settings minus release and iteration...</h3>')
            for field in fields:
                _, _, owner_reviewer_search_criteria = self.get_filtered_search_criteria(session_document, required_states)
                for attribute in ['release', 'iteration']:
                    if attribute in owner_reviewer_search_criteria:
                        del owner_reviewer_search_criteria[attribute]

                owner_reviewer_search_criteria[field] = re.compile('(?i)'+search)
                for document in self.cards_collection.find(owner_reviewer_search_criteria):
                    if 'lastchanged' in document and document['lastchanged']:
                        sortable_documents.append((document['lastchanged'], field, document))
                    else:
                        sortable_documents.append((0, field, document))

        # Search with current filter bar settings minus user
        if not sortable_documents:
            content.append('<h3>Searching under current filter bar settings minus user...</h3>')
            for field in fields:
                _, _, owner_reviewer_search_criteria = self.get_filtered_search_criteria(session_document, required_states)
                del owner_reviewer_search_criteria['$or']
                owner_reviewer_search_criteria[field] = re.compile('(?i)'+search)
                for document in self.cards_collection.find(owner_reviewer_search_criteria):
                    if 'lastchanged' in document and document['lastchanged']:
                        sortable_documents.append((document['lastchanged'], field, document))
                    else:
                        sortable_documents.append((0, field, document))

        # Search with current filter bar settings minus user, release and iteration
        if not sortable_documents:
            content.append('<h3>Searching under current filter bar settings minus user, release and iteration...</h3>')
            for field in fields:
                _, _, owner_reviewer_search_criteria = self.get_filtered_search_criteria(session_document, required_states)
                del owner_reviewer_search_criteria['$or']
                for attribute in ['release', 'iteration']:
                    if attribute in owner_reviewer_search_criteria:
                        del owner_reviewer_search_criteria[attribute]

                owner_reviewer_search_criteria[field] = re.compile('(?i)'+search)
                for document in self.cards_collection.find(owner_reviewer_search_criteria):
                    if 'lastchanged' in document and document['lastchanged']:
                        sortable_documents.append((document['lastchanged'], field, document))
                    else:
                        sortable_documents.append((0, field, document))

        # Search with current filter bar settings minus type
        if not sortable_documents:
            content.append('<h3>Searching under current filter bar settings minus type...</h3>')
            for field in fields:
                _, _, owner_reviewer_search_criteria = self.get_filtered_search_criteria(session_document, required_states)
                if 'type' in owner_reviewer_search_criteria:
                    del owner_reviewer_search_criteria['type']

                owner_reviewer_search_criteria[field] = re.compile('(?i)'+search)
                for document in self.cards_collection.find(owner_reviewer_search_criteria):
                    if 'lastchanged' in document and document['lastchanged']:
                        sortable_documents.append((document['lastchanged'], field, document))
                    else:
                        sortable_documents.append((0, field, document))

        # Search with current filter bar settings minus state
        if not sortable_documents:
            content.append('<h3>Searching under current filter bar settings minus state...</h3>')
            for field in fields:
                _, _, owner_reviewer_search_criteria = self.get_filtered_search_criteria(session_document, required_states)
                if 'state' in owner_reviewer_search_criteria:
                    del owner_reviewer_search_criteria['state']

                owner_reviewer_search_criteria[field] = re.compile('(?i)'+search)
                for document in self.cards_collection.find(owner_reviewer_search_criteria):
                    if 'lastchanged' in document and document['lastchanged']:
                        sortable_documents.append((document['lastchanged'], field, document))
                    else:
                        sortable_documents.append((0, field, document))

        # Search with current filter bar settings minus category
        if not sortable_documents:
            content.append('<h3>Searching under current filter bar settings minus category...</h3>')
            for field in fields:
                _, _, owner_reviewer_search_criteria = self.get_filtered_search_criteria(session_document, required_states)
                if 'category' in owner_reviewer_search_criteria:
                    del owner_reviewer_search_criteria['category']

                owner_reviewer_search_criteria[field] = re.compile('(?i)'+search)
                for document in self.cards_collection.find(owner_reviewer_search_criteria):
                    if 'lastchanged' in document and document['lastchanged']:
                        sortable_documents.append((document['lastchanged'], field, document))
                    else:
                        sortable_documents.append((0, field, document))

        # Search with current filter bar settings minus customer
        if not sortable_documents:
            content.append('<h3>Searching under current filter bar settings minus customer...</h3>')
            for field in fields:
                _, _, owner_reviewer_search_criteria = self.get_filtered_search_criteria(session_document, required_states)
                if 'customer' in owner_reviewer_search_criteria:
                    del owner_reviewer_search_criteria['customer']

                owner_reviewer_search_criteria[field] = re.compile('(?i)'+search)
                for document in self.cards_collection.find(owner_reviewer_search_criteria):
                    if 'lastchanged' in document and document['lastchanged']:
                        sortable_documents.append((document['lastchanged'], field, document))
                    else:
                        sortable_documents.append((0, field, document))

        if sortable_documents:
            content.append(self.display_search_results(project, highlight_attrs_values, sortable_documents))
        else:
            content.append('<h3>Sorry, nothing found! Please change your filter settings and try again!</h3>')

        content.append('</div>')
        content.append('<script type="text/javascript" src="/scripts/listview.js"></script>')
        content.append(Kanbanara.footer(self))
        return "".join(content)

    @cherrypy.expose
    def search_autocomplete(self, term):
        """comment"""
        session_id = Kanbanara.cookie_handling(self)
        matches = []
        session_document = self.sessions_collection.find_one({"session_id": session_id})
        required_columns, required_states = self.get_displayable_columns()
        for attribute in ['creator', 'description', 'externalreference', 'notes', 'status', 'title']:
            _, _, owner_reviewer_search_criteria = self.get_filtered_search_criteria(session_document, required_states)
            owner_reviewer_search_criteria[attribute] = re.compile('(?i)'+term)
            matches += self.cards_collection.find(owner_reviewer_search_criteria).distinct(attribute)

        content = []
        content.append('[')
        for match_no, match in enumerate(matches):
            content.append('"'+match.replace('"', '\\"')+'"')
            if match_no < len(matches)-1:
                content.append(',')

        content.append(']')
        return "".join(content)

currentDir = os.path.dirname(os.path.abspath(__file__))
conf = {'/': {'tools.staticdir.root':   currentDir,
              'tools.sessions.on':      True,
              'tools.sessions.locking': 'explicit'
              }}
for directory in ['css', 'images']:
    if os.path.exists(currentDir+os.sep+directory):
        conf['/'+directory] = {'tools.staticdir.on':  True,
                               'tools.staticdir.dir': directory}

cherrypy.tree.mount(Search(), '/search', config=conf)
