"""
Generic typeahead document chain.

The generic typeahead view is designed to return *all* relevant results for a
user query and leaves the final filtering and sorting of the results to the
client-side caller.

State configuration options:

: `collation`
    The collation to use when querying for results.

: `hint`
    A hint to use when querying for results.

: `projection`
    The projection used when requesting results from the database (defaults to
    None which means the detault projection for the frame class will be used).

: `typeahead_field`
    The document field to query against (required).

: `typeahead_id_attr`
    The attribute against each document that will be used to extract an Id value
    for the typeahead results (defaults to '_id').

: `typeahead_label_attr`
    The attribute against each document that will be used to extract a label
    value for the typeahead results (if not specified the lable will be
    generated by coercing the document to a string (e.g `str(document)`).

: `typeahead_match_len`
    The minimum length of the query string before the typeahead view will
    attempt to perform a match (defaults to 2).

: `typeahead_match_type`
    The type of match the type ahead will perform when querying results
    (defaults to 'startswith', must be either 'startswith' or 'contains').

"""

import bson
import re

import flask
from manhattan.chains import Chain, ChainMgr
from manhattan import formatters
from mongoframes import ASC, DESC, InvalidPage, Or, Paginator, Q

from . import factories
from .utils import json_fail, json_success

__all__ = ['typeahead_chains']


# Define the chains
typeahead_chains = ChainMgr()

# GET
typeahead_chains['get'] = Chain([
    'config',
    'authenticate',
    'search',
    'select',
    'render_json'
])


# Define the links
typeahead_chains.set_link(factories.config(
    collation=None,
    hint=None,
    projection=None,
    typeahead_field=None,
    typeahead_id_attr='_id',
    typeahead_label_attr=None,
    typeahead_match_len=2,
    typeahead_match_type='startswith'
    ))
typeahead_chains.set_link(factories.authenticate())

@typeahead_chains.link
def search(state):
    """
    Build a database query based on the `q` parameter within the request to
    filter the typeahead results.

    This link adds the `query` key to the state containing the database query.
    """
    state.query = None

    # If the query isn't long enough to query the database then return a failed
    # response.
    q = flask.request.args.get('q', '').strip()
    if len(q) < state.typeahead_match_len:
        return json_fail(
            'The query string is too short, it should be at least {0} '
            'characters long'.format(state.typeahead_match_len)
        )

    # Replace accented characters in the string with closest matching
    # equivalent non-accented characters.
    q = formatters.text.remove_accents(q)

    # Shorten the query string to the match length
    q = q[:state.typeahead_match_len]

    # Make the query safe for regular expressions
    q = re.escape(q)

    # Build the query to search
    assert state.typeahead_field, 'No typeahead field defined'

    if state.typeahead_match_type == 'contains':
        state.query = Q[state.typeahead_field] == re.compile(q, re.I)
    elif state.typeahead_match_type == 'startswith':
        state.query = Q[state.typeahead_field] == re.compile(r'^' + q, re.I)

    assert state.query, "'{0}' is not a valid typeahead match type"

@typeahead_chains.link
def select(state):
    """
    Select the documents for the typeahead.

    This link adds the `{state.manage_config.var_name_plural}` key to the state
    containing the selected database documents.
    """

    # Select the documents
    many_kw = {}
    if state.projection:
        many_kw['projection'] = state.projection

    if state.collation:
        many_kw['collation'] = state.collation

    if state.hint:
        many_kw['hint'] = state.hint

    documents = state.manage_config.frame_cls.many(state.query, **many_kw)

    state[state.manage_config.var_name_plural] = documents

@typeahead_chains.link
def render_json(state):
    """
    Convert the selected documents to a JSON result for the client-side
    typeahead.
    """

    # Convert the matching documents into a list of suggestions for the typeahead
    suggestions = []
    for document in state[state.manage_config.var_name_plural]:
        suggestion = {}

        # Id
        suggestion['value'] = getattr(document, state.typeahead_id_attr)

        # If the Id is and ObjectId then we need to convert it to a string for
        # JSON.
        if isinstance(suggestion['value'], bson.ObjectId):
            suggestion['value'] = str(suggestion['value'])

        # Label
        if state.typeahead_label_attr:
            suggestion['label'] = getattr(
                document,
                state.typeahead_label_attr
            )
        else:
            suggestion['label'] = str(document)

        suggestions.append(suggestion)

    return json_success({'suggestions': suggestions})
