# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

import contextlib
import logging

import psycopg2

import socorro.database.database as db
from socorro.external import DatabaseError

from .dbapi2_util import execute_query_fetchall, single_value_sql

logger = logging.getLogger("webapi")


def add_param_to_dict(dictionary, key, value):
    """
    Dispatch a list of parameters into a dictionary.
    """
    for i, elem in enumerate(value):
        dictionary[key + str(i)] = elem
    return dictionary


class PostgreSQLBase(object):

    """
    Base class for PostgreSQL based service implementations.
    """

    def __init__(self, *args, **kwargs):
        """
        Store the config and create a connection to the database.

        Keyword arguments:
        config -- Configuration of the application.

        """
        self.context = kwargs.get("config")
        if hasattr(self.context, 'database'):
            # XXX this should be replaced with connection_context instead
            self.context.database['database_host'] = \
                self.context.database.database_hostname
            self.context.database['database_port'] = \
                self.context.database.database_port
            self.context.database['database_name'] = \
                self.context.database.database_name
            self.context.database['database_username'] = \
                self.context.database.database_username
            self.context.database['database_password'] = \
                self.context.database.database_password
            self.database = db.Database(self.context.database)
        else:
            # the old middleware
            self.database = db.Database(self.context)

    @contextlib.contextmanager
    def get_connection(self):
        connection = self.database.connection()
        try:
            yield connection
        finally:
            connection.close()

    def query(self, sql, params=None, error_message=None, connection=None):
        """Return the result of a query executed against PostgreSQL.

        Create a connection, open a cursor, execute the query and return the
        results. If an error occures, log it and raise a DatabaseError.

        Keyword arguments:
        sql -- SQL query to execute.
        params -- Parameters to merge into the SQL query when executed.
        error_message -- Eventual error message to log.
        connection -- Optional connection to the database. If none, a new one
                      will be opened.

        """
        fresh_connection = False
        try:
            if not connection:
                connection = self.database.connection()
                fresh_connection = True
            # self.context.logger.debug(connection.cursor.mogrify(sql, params))
            results = execute_query_fetchall(connection, sql, params)
        except psycopg2.Error, e:
            if error_message is None:
                error_message = "Failed to execute query against PostgreSQL"
            error_message = "%s - %s" % (error_message, str(e))
            logger.error(error_message, exc_info=True)
            raise DatabaseError(error_message)
        finally:
            if connection and fresh_connection:
                connection.close()

        return results

    def count(self, sql, params=None, error_message=None, connection=None):
        """Return the result of a count SQL query executed against PostgreSQL.

        Create a connection, open a cursor, execute the query and return the
        result. If an error occures, log it and raise a DatabaseError.

        Keyword arguments:
        sql -- SQL query to execute.
        params -- Parameters to merge into the SQL query when executed.
        error_message -- Eventual error message to log.
        connection -- Optional connection to the database. If none, a new one
                      will be opened.

        """
        fresh_connection = False
        try:
            if not connection:
                connection = self.database.connection()
                fresh_connection = True
            # self.context.logger.debug(connection.cursor.mogrify(sql, params))
            result = single_value_sql(connection, sql, params)
        except psycopg2.Error, e:
            if error_message is None:
                error_message = "Failed to execute count against PostgreSQL"
            error_message = "%s - %s" % (error_message, str(e))
            logger.error(error_message, exc_info=True)
            raise DatabaseError(error_message)
        finally:
            if connection and fresh_connection:
                connection.close()

        return result

    @staticmethod
    def parse_versions(versions_list, products):
        """
        Parses the versions, separating by ":" and returning versions
        and products.
        """
        versions = []

        for v in versions_list:
            if v.find(":") > -1:
                pv = v.split(":")
                versions.append(pv[0])
                versions.append(pv[1])
            else:
                products.append(v)

        return (versions, products)

    @staticmethod
    def prepare_terms(terms, search_mode):
        """
        Prepare terms for search, adding '%' where needed,
        given the search mode.
        """
        if search_mode in ("contains", "starts_with"):
            terms = terms.replace("_", "\_").replace("%", "\%")

        if search_mode == "contains":
            terms = "%" + terms + "%"
        elif search_mode == "starts_with":
            terms = terms + "%"
        return terms

    @staticmethod
    def dispatch_params(sql_params, key, value):
        """
        Dispatch a parameter or a list of parameters into the params array.
        """
        if not isinstance(value, list):
            sql_params[key] = value
        else:
            for i, elem in enumerate(value):
                sql_params[key + str(i)] = elem
        return sql_params

    @staticmethod
    def build_reports_sql_from(params):
        """
        Generate and return the FROM part of the final SQL query.
        """
        sql_from = ["FROM reports r"]

        ## Searching through plugins
        if params["report_process"] == "plugin":
            sql_from.append(("plugins_reports ON "
                             "plugins_reports.report_id = r.id"))
            sql_from.append(("plugins ON "
                             "plugins_reports.plugin_id = plugins.id"))

        sql_from = " JOIN ".join(sql_from)
        return sql_from

    @staticmethod
    def build_reports_sql_where(params, sql_params, config):
        """
        """
        sql_where = ["""
            WHERE r.date_processed BETWEEN %(from_date)s AND %(to_date)s
        """]

        sql_params["from_date"] = params["from_date"]
        sql_params["to_date"] = params["to_date"]

        ## Adding terms to where clause
        if params["terms"]:
            if params["search_mode"] == "is_exactly":
                sql_where.append("r.signature=%(term)s")
            else:
                sql_where.append("r.signature LIKE %(term)s")
            sql_params["term"] = params["terms"]

        ## Adding products to where clause
        if params["products"]:
            products_list = ["r.product=%(product" + str(x) + ")s"
                             for x in range(len(params["products"]))]
            sql_where.append("(%s)" % (" OR ".join(products_list)))
            sql_params = add_param_to_dict(sql_params, "product",
                                           params["products"])

        ## Adding OS to where clause
        if params["os"]:
            os_list = ["r.os_name=%(os" + str(x) + ")s"
                       for x in range(len(params["os"]))]
            sql_where.append("(%s)" % (" OR ".join(os_list)))
            sql_params = add_param_to_dict(sql_params, "os", params["os"])

        ## Adding versions to where clause
        if params["versions"]:
            sql_params = add_param_to_dict(sql_params, "version",
                                           params["versions"])

            versions_where = []

            for x in range(0, len(params["versions"]), 2):
                version_where = []
                version_where.append(str(x).join(("r.product=%(version",
                                                  ")s")))

                key = "%s:%s" % (params["versions"][x],
                                 params["versions"][x + 1])
                version_where = PostgreSQLBase.build_reports_sql_version_where(
                    key,
                    params,
                    config,
                    x,
                    sql_params,
                    version_where
                )

                version_where.append(str(x + 1).join((
                                     "r.version=%(version", ")s")))
                versions_where.append("(%s)" % " AND ".join(version_where))

            sql_where.append("(%s)" % " OR ".join(versions_where))

        ## Adding build id to where clause
        if params["build_ids"]:
            build_ids_list = ["r.build=%(build" + str(x) + ")s"
                              for x in range(len(params["build_ids"]))]
            sql_where.append("(%s)" % (" OR ".join(build_ids_list)))
            sql_params = add_param_to_dict(sql_params, "build",
                                           params["build_ids"])

        ## Adding reason to where clause
        if params["reasons"]:
            reasons_list = ["r.reason=%(reason" + str(x) + ")s"
                            for x in range(len(params["reasons"]))]
            sql_where.append("(%s)" % (" OR ".join(reasons_list)))
            sql_params = add_param_to_dict(sql_params, "reason",
                                           params["reasons"])

        ## Adding release channels to where clause
        if params["release_channels"]:
            channels_list = [
                "UPPER(r.release_channel)=UPPER(%%(release_channel%s)s)" % x
                for x in range(len(params["release_channels"]))
            ]
            sql_where.append("(%s)" % " OR ".join(channels_list))
            sql_params = add_param_to_dict(
                sql_params,
                "release_channel",
                params["release_channels"]
            )

        ## Adding report type to where clause
        if params["report_type"] == "crash":
            sql_where.append("r.hangid IS NULL")
        elif params["report_type"] == "hang":
            sql_where.append("r.hangid IS NOT NULL")

        ## Searching through plugins
        if params["report_process"] == "plugin":
            sql_where.append("r.process_type = 'plugin'")
            sql_where.append(("plugins_reports.date_processed BETWEEN "
                              "%(from_date)s AND %(to_date)s"))

            if params["plugin_terms"]:
                comp = "="

                if params["plugin_search_mode"] in ("contains", "starts_with"):
                    comp = " LIKE "

                sql_where_plugin_in = []
                for f in params["plugin_in"]:
                    if f == "name":
                        field = "plugins.name"
                    elif f == "filename":
                        field = "plugins.filename"

                    sql_where_plugin_in.append(comp.join((field,
                                                          "%(plugin_term)s")))
                sql_params["plugin_term"] = params["plugin_terms"]

                sql_where.append("(%s)" % " OR ".join(sql_where_plugin_in))

        elif params["report_process"] == "browser":
            sql_where.append("r.process_type IS NULL")

        elif params["report_process"] == "content":
            sql_where.append("r.process_type = 'content'")

        sql_where = " AND ".join(sql_where)
        return (sql_where, sql_params)

    @staticmethod
    def build_reports_sql_limit(params, sql_params):
        """
        """
        sql_limit = """
            LIMIT %(limit)s
            OFFSET %(offset)s
        """
        sql_params["limit"] = params["result_number"]
        sql_params["offset"] = params["result_offset"]

        return (sql_limit, sql_params)

    @staticmethod
    def build_reports_sql_version_where(key, params, config, x, sql_params,
                                        version_where):
        """
        Return a list of strings for version restrictions.
        """
        if key in params["versions_info"]:
            version_info = params["versions_info"][key]
        else:
            version_info = None

        if x is None:
            version_param = "version"
        else:
            version_param = "version%s" % (x + 1)

        if hasattr(config, 'webapi'):
            context = config.webapi
        else:
            # old middleware
            context = config

        if version_info and version_info["release_channel"]:
            channel = version_info["release_channel"].lower()
            if channel.startswith(tuple(context.channels)):
                # Use major_version instead of full version
                sql_params[version_param] = version_info["major_version"]
                # Restrict by release_channel
                version_where.append("r.release_channel ILIKE '%s'" % channel)
                if channel.startswith(tuple(context.restricted_channels)):
                    # Restrict to a list of build_id
                    version_where.append("r.build IN ('%s')" % (
                        "', '".join([
                            str(bid) for bid in version_info["build_id"]])))

            else:
                # it's a release
                version_where.append(("r.release_channel NOT IN %s" %
                                      (tuple(context.channels),)))

        return version_where
