from pyramid.config import Configurator

from sqlalchemy import engine_from_config

from .models import (
    get_user_by_id,
    get_or_create_user_by_email,
    get_node_by_id,
    get_or_create_node_by_uri,
    get_nodes_who_share_root,
    get_node_id_map,
    get_graph_from_nodes,
    get_or_create_namespace,
    get_namespace_request_by_id,
    get_conversation_graph_from_nodes,
    flatten_graph,
)

# reject spammers.
from pyramid.httpexceptions import HTTPUnauthorized

# cookie only session, not encrypted but signed to prevent tampering!
from pyramid.session import SignedCookieSessionFactory

# email validation.
import re

# needed to load themes.
from pkg_resources import iter_entry_points

# needed to support expanding ENV vars from ini.
from os.path import expandvars

import logging

log = logging.getLogger(__name__)

JINJA2_EXTENSION = ".j2"


def get_int_or_bool_or_none_or_str(value):
    """
    Given a string value pulled from a configuration file,
    this function attempts to return the value with the proper type.
    """
    try:
        return int(value)
    except ValueError:
        if value.lower() in {"yes", "y", "true", "y"}:
            return True
        elif value.lower() in {"no", "n", "false", "f"}:
            return False
        elif value.lower() == "none":
            return None
        return str(value)


def get_children_settings(settings, parent_key):
    """
    Accept a settings dict and parent key, return dict of children

    For example:

      auth_tkt.hashalg = md5

    Results to:

      {'auth_tkt.hashalg': 'md5'}

    This function returns the following:

      >>> get_children_settings({'auth_tkt.hashalg': 'md5'}, 'auth_tkt')
      {'hashalg': 'md5'}

    """
    # needed to support expanding ENV vars from ini.
    from os.path import expandvars

    # the +1 is the . between parent and child settings.
    parent_len = len(parent_key) + 1
    children = {}
    for key, value in settings.items():
        if parent_key in key:
            # expandvars replaces template with ENV vars.
            children[key[parent_len:]] = get_int_or_bool_or_none_or_str(
                expandvars(value)
            )
    return children


def load_entry_points(group_name):
    """Return a dictionary of entry_points related to given group_name"""
    entry_points = {}
    for entry_point in iter_entry_points(group=group_name, name=None):
        entry_points[entry_point.name] = entry_point.load()
    return entry_points


def load_jinja2_themes(config):
    """Automatically load any entry_point registered Remarkbox theme."""
    themes = load_entry_points("remarkbox.themes")
    for theme_name, theme_module in themes.items():
        theme_module_name = theme_module.__name__
        # teach Jinja2 about the template dir in the theme package.
        config.add_jinja2_search_path(
            "{}:templates/".format(theme_module_name), name=JINJA2_EXTENSION
        )
        # teach Pyramid about the static assets in the theme package.
        config.add_static_view(
            "static/theme/{}".format(theme_name),
            "{}:static/theme/{}".format(theme_module_name, theme_name),
            cache_max_age=3600,
        )
    return config


def is_ipv4(string):
    try:
        parts = list(map(int, string.split(".")))
    except:
        return False
    if len(parts) != 4:
        return False
    for part in parts:
        if part < 0 or part > 256:
            return False
    return True


def maybe_root_domain(string):
    """
    Maybe get valid "root" domain from given string, for example -

    valid:

      * hi.www.example.com -> example.com
      * www.example.com -> example.com
      * example.com -> example.com

    invalid:

      * comments.cryptocoin.com.au -> com.au
    """
    return ".".join(string.split(".")[-2:])


def main(global_config, **settings):
    """This function returns a Pyramid WSGI application."""

    app_settings = get_children_settings(settings, "app")
    session_settings = get_children_settings(settings, "session")

    def wild_signed_cookie_session_factory(request):
        """
        Hack SignedCookieSessionFactory to have `wild_domain`.
        Pyramid devs do not want to support this.

        In addition the domain is determined per request!

        This means a single deployment of this app can support managing a
        `wild_domain` cookie for any domain (likely using CNAMES).
        """
        root_domain = maybe_root_domain(request.domain)
        if is_ipv4(request.domain) or root_domain.startswith("com"):
            session_settings["domain"] = request.domain
        else:
            session_settings["domain"] = root_domain

        factory = SignedCookieSessionFactory(**session_settings)
        return factory(request)

    # setup session factory to use unencrypted but signed cookies.
    # session_factory = SignedCookieSessionFactory(**session_settings)
    session_factory = wild_signed_cookie_session_factory

    # build app config object from ini.
    config = Configurator(settings=settings, session_factory=session_factory)

    # setup and require automatic CSRF checking.
    config.set_default_csrf_options(require_csrf=True)

    # Create database engine from connection details in ini.
    # make request.dbsession available for use in Pyramid.
    config.include(".models")

    # setup jinja2 template support.
    config.include("pyramid_jinja2")
    config.add_jinja2_search_path("remarkbox:templates/", name=JINJA2_EXTENSION)
    config.add_jinja2_renderer(JINJA2_EXTENSION)
    config = load_jinja2_themes(config)

    # compile the email validator regex outside of the functions.
    _email_regex = re.compile("^[^@]+@[^@]+\.[^.@]+$")

    def add_debug_mode(request):
        """Return True if debug toolbar is enabled."""
        return "pyramid_debugtoolbar" in request.registry.settings.get(
            "pyramid.includes", ""
        )

    '''
    def add_redis(request):
        """Return Redis Connection"""
        redis_host = request.app.get("redis.host", "localhost")
        redis_port = request.app.get("redis.port", 6379)
        redis_db = request.app.get("redis.db", 0)
        return redis.Redis(host=redis_host, port=redis_port, db=redis_db)
    '''

    def add_email(request):
        """Return Email or None. Email must pass regex."""
        email = request.params.get("email", "")
        if _email_regex.match(email) is not None:
            return email

    def add_user(request):
        """Return User object or None. User.authenticated may be True or False."""
        user = None
        authenticated_user_id = request.session.get("authenticated_user_id", None)

        if authenticated_user_id:
            # attach the user object from DB to the request.
            user = get_user_by_id(request.dbsession, authenticated_user_id)
            if user is not None:
                user.authenticated = True

        elif request.email:
            user = get_or_create_user_by_email(request.dbsession, request.email)

        return user

    def add_csrf_token(request):
        if request.user and request.user.authenticated:
            return request.session.get_csrf_token()

    def add_node(request):
        """Return Node object or None from matchdict or params."""
        thread_uri = request.params.get("thread_uri")
        thread_title = request.params.get("thread_title")
        node_id = request.matchdict.get("node_id")
        nojs = request.params.get("nojs", False)

        node = None

        # TODO: thread_uri must take priority over node_id because our forms
        # are dumb. If we fix our forms to only pass node_id/node_path when not
        # None, the priority / order will not matter.
        if thread_uri:
            # Remove "/reply" from the end of the URL if present
            url = request.url
            if url.endswith("/reply"):
                url = url[:-6]  # Remove the last 6 characters ("/reply")
            request.session["back_to_thread"] = url

            node = get_or_create_node_by_uri(
                request.dbsession, thread_uri, thread_title
            )
        elif node_id:
            node = get_node_by_id(request.dbsession, node_id)
        elif nojs and request.referer:
            # if javascript is disabled, use the referer.
            node = get_or_create_node_by_uri(request.dbsession, request.referer)

        return node

    def add_root_node(request):
        return request.node.root

    def add_nodes(request):
        if request.node:
            return get_nodes_who_share_root(
                request.dbsession,
                request.node.root,
                request.node_order,
            )

    def add_node_id_map(request):
        return get_node_id_map(request.nodes)

    def add_node_graph(request):
        return get_graph_from_nodes(request.nodes)

    def add_node_flat_graph(request):
        return flatten_graph(request.node.root.id, request.node_graph)

    def add_conversation_graph(request):
        return get_conversation_graph_from_nodes(
            request.nodes,
            graph=request.node_graph,
            node_id_map=request.node_id_map,
            order=request.node_order,
        )

    def add_namespace(request):
        namespace_name = request.matchdict.get(
            "namespace", request.params.get("namespace", None)
        )
        if namespace_name:
            return get_or_create_namespace(request.dbsession, namespace_name)
        elif request.node:
            return request.node.root.namespace
        return get_or_create_namespace(request.dbsession, request.domain)

    def add_mode(request):
        """return mode of 'embed' or 'basic'"""
        return "embed" if "embed" in request.matched_route.name else "basic"

    def add_link_prefix(request):
        if request.mode == "embed":
            return "/embed/ns/{}".format(request.namespace.name)
        return ""

    def add_spam(request):
        """Test if request looks spammy HTTP Error or False."""
        if request.params.get("email2", "") != "":
            # spam filter, hidden email2 field tricks bots.
            return HTTPUnauthorized("you smell like a spammer")
        return False

    def add_owner_key(request):
        return request.params.get("rb_owner_key", None)

    def add_namespace_request(request):
        """Add namespace_request if pending, else None"""
        # we reduce database lookups by only querying when owner_key is
        # present and request.namespace has a pending owner_request.
        if request.owner_key and request.namespace.owner_request_pending:
            namespace_request = get_namespace_request_by_id(
                request.dbsession, request.owner_key
            )
            # for dubugging purposes only.
            # log.info("owner_key={}, pending={}".format(request.owner_key,request.namespace.owner_request_pending))
            if namespace_request and namespace_request.namespace == request.namespace:
                return namespace_request

    def add_app(request):
        """Attach app settings dictionary."""
        return app_settings

    def add_app_url(request):
        """
        Use the app_url from config if request.domain ends with the
        configured root_domain. Otherwise just use the request's host_url
        """
        config_app_url = request.app.get("app_url", request.host_url)
        root_domain = request.app.get("root_domain")
        if root_domain and request.domain.endswith(root_domain):
            return config_app_url
        elif request.domain == "localhost":
            return config_app_url
        return request.host_url

    def add_app_domain(request):
        return request.app_url.split("://")[-1].split(":")[0]

    def add_marketing_url(request):
        return request.app.get("marketing_url", request.app_url)

    def add_marketing_domain(request):
        return request.marketing_url.split("://")[-1].split(":")[0]

    def add_faq_home(request):
        """Return True if this request's domain equals namespace name."""
        return request.domain == request.namespace.name

    def add_saas_home(request):
        """Only one domain should have this method return True per deployment."""
        root_domain = request.app.get("root_domain")
        return (
            root_domain
            and request.domain.endswith(root_domain)
            and request.app_domain == request.namespace.name
        )

    def add_stripe(request):
        """Attach a stripe object with creds to request."""
        import stripe

        stripe.api_key = request.app.get("stripe.secret")
        return stripe

    def add_stripe_customer(request):
        if request.user:
            if not request.user.stripe_id:
                # create a new stripe customer.
                customer = request.stripe.Customer.create(email=request.user.email)
                request.user.stripe_id = customer.id
                request.dbsession.add(request.user)
                request.dbsession.flush()
            return request.stripe.Customer.retrieve(request.user.stripe_id)
        return None

    def add_stripe_saved_cards(request):
        if request.user and request.user.stripe_id:
            return request.stripe_customer.sources
        return []

    def add_stripe_active_card(request):
        if request.user and request.user.stripe_id:
            if request.stripe_customer.default_source:
                return request.stripe_customer.sources.retrieve(
                    request.stripe_customer.default_source
                )
        return None

    def add_avatar_size(request):
        """Attach avatar size or default."""
        if request.namespace:
            if request.namespace.avatar_size:
                return request.namespace.avatar_size
        return request.app.get("avatar.size", 30)

    def add_stand_alone_mode(request):
        if request.app.get("stand_alone_mode", "disabled") == "enabled":
            return True
        return False

    def add_theme(request):
        """Attach theme name or None."""
        if request.mode == "embed":
            if request.namespace and request.namespace.theme_embed:
                return request.namespace.theme_embed
            return request.app.get("theme_embed", None)

        if request.namespace and request.namespace.theme:
            return request.namespace.theme
        return request.app.get("theme", None)

    def add_stylesheet(request):
        """Attach stylesheet or empty string."""
        if request.mode == "embed":
            return request.stylesheet_embed
        return request.stylesheet_basic

    def add_stylesheet_basic(request):
        """Attach stylesheet or empty string."""
        if request.namespace and request.namespace.stylesheet:
            return request.namespace.stylesheet
        return ""

    def add_stylesheet_embed(request):
        """Attach stylesheet_embed or empty string."""
        if request.namespace and request.namespace.stylesheet_embed:
            return request.namespace.stylesheet_embed
        return ""

    def add_stylesheet_uri(request):
        """Attach stylesheet uri or empty string."""
        if request.mode == "embed":
            return request.stylesheet_embed_uri
        return request.stylesheet_basic_uri

    def add_stylesheet_basic_uri(request):
        """Attach stylesheet uri or empty string."""
        if request.namespace and request.namespace.stylesheet_uri:
            return request.namespace.stylesheet_uri
        return ""

    def add_stylesheet_embed_uri(request):
        """Attach stylesheet_embed uri or empty string."""
        if request.namespace and request.namespace.stylesheet_embed_uri:
            return request.namespace.stylesheet_embed_uri
        return ""

    def add_base_template(request):
        """Attach base_template filename."""
        if request.theme:
            return "{}-base.j2".format(request.theme)
        return "base.j2"

    def add_base_funnel_template(request):
        """Attach base_funnel template filename."""
        if request.theme:
            return "{}-base-funnel.j2".format(request.theme)
        return "base.j2"

    def add_page_number(request):
        """Attach page_number starting at 0"""
        page_number = int(request.params.get("page", 1))
        return page_number if page_number >= 1 else 1

    def add_page_size(request):
        # if we want to support custom page sizes, set it here.
        return 100

    def add_page_offset(request):
        return (request.page_number - 1) * request.page_size

    def add_node_order(request):
        return request.params.get("order", request.namespace.node_order)

    def add_mathjax(request):
        return "true" if request.namespace.mathjax else "false"

    # register functions to app config as request methods.
    # each request instance will run these functions and attach results.
    # cache result with `reify=True` to prevent multiple db lookups.
    # config.add_request_method(add_redis, "redis", reify=True)
    config.add_request_method(add_debug_mode, "debug_mode", reify=True)
    config.add_request_method(add_user, "user", reify=True)
    config.add_request_method(add_csrf_token, "csrf_token", reify=True)
    config.add_request_method(add_email, "email", reify=True)
    config.add_request_method(add_node, "node", reify=True)
    config.add_request_method(add_root_node, "root_node", reify=True)
    config.add_request_method(add_nodes, "nodes", reify=True)
    config.add_request_method(add_node_id_map, "node_id_map", reify=True)
    config.add_request_method(add_node_graph, "node_graph", reify=True)
    config.add_request_method(add_node_flat_graph, "node_flat_graph", reify=True)
    config.add_request_method(add_conversation_graph, "conversation_graph", reify=True)
    config.add_request_method(add_namespace, "namespace", reify=True)
    config.add_request_method(add_mode, "mode", reify=True)
    config.add_request_method(add_link_prefix, "link_prefix", reify=True)
    config.add_request_method(add_spam, "spam", reify=True)
    config.add_request_method(add_owner_key, "owner_key", reify=True)
    config.add_request_method(add_namespace_request, "namespace_request", reify=True)
    config.add_request_method(add_app, "app", reify=True)
    config.add_request_method(add_app_url, "app_url", reify=True)
    config.add_request_method(add_app_domain, "app_domain", reify=True)
    config.add_request_method(add_marketing_url, "marketing_url", reify=True)
    config.add_request_method(add_marketing_domain, "marketing_domain", reify=True)
    config.add_request_method(add_faq_home, "faq_home", reify=True)
    config.add_request_method(add_saas_home, "saas_home", reify=True)
    config.add_request_method(add_stripe, "stripe", reify=True)
    config.add_request_method(add_stripe_customer, "stripe_customer", reify=True)
    config.add_request_method(add_stripe_saved_cards, "stripe_saved_cards", reify=True)
    config.add_request_method(add_stripe_active_card, "stripe_active_card", reify=True)
    config.add_request_method(add_stand_alone_mode, "stand_alone_mode", reify=True)
    config.add_request_method(add_avatar_size, "avatar_size", reify=True)
    config.add_request_method(add_theme, "theme", reify=True)
    config.add_request_method(add_stylesheet_basic, "stylesheet_basic", reify=True)
    config.add_request_method(add_stylesheet_embed, "stylesheet_embed", reify=True)
    config.add_request_method(add_stylesheet, "stylesheet", reify=True)
    config.add_request_method(
        add_stylesheet_basic_uri, "stylesheet_basic_uri", reify=True
    )
    config.add_request_method(
        add_stylesheet_embed_uri, "stylesheet_embed_uri", reify=True
    )
    config.add_request_method(add_stylesheet_uri, "stylesheet_uri", reify=True)
    config.add_request_method(add_base_template, "base_template", reify=True)
    config.add_request_method(
        add_base_funnel_template, "base_funnel_template", reify=True
    )
    config.add_request_method(add_page_number, "page_number", reify=True)
    config.add_request_method(add_page_size, "page_size", reify=True)
    config.add_request_method(add_page_offset, "page_offset", reify=True)
    config.add_request_method(add_node_order, "node_order", reify=True)
    config.add_request_method(add_mathjax, "mathjax", reify=True)

    # all of the web application routes.
    config.include(".routes")

    # Scan for views.
    config.scan()

    return config.make_wsgi_app()
