# copyright 2003-2016 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact https://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
#
# CubicWeb is free software: you can redistribute it and/or modify it under the
# terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 2.1 of the License, or (at your option)
# any later version.
#
# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb.  If not, see <https://www.gnu.org/licenses/>.
"""Server subcube of cubicweb : defines objects used only on the server
(repository) side

The server module contains functions to initialize a new repository.
"""
from contextlib import contextmanager

from logilab import database
from logilab.common.modutils import LazyObject
from logilab.common.registry import yes
from logilab.common.textutils import splitstrip
from yams import BASE_GROUPS

from cubicweb.appobject import AppObject


# server-side services #########################################################


class Service(AppObject):
    """Base class for services.

    A service is a selectable object that performs an action server-side.
    Use :class:`cubicweb.dbapi.Connection.call_service` to call them from
    the web-side.

    When inheriting this class, do not forget to define at least the __regid__
    attribute (and probably __select__ too).
    """

    __registry__ = "services"
    __select__ = yes()

    def call(self, **kwargs):
        raise NotImplementedError


# server-side debugging ########################################################

# server debugging flags. They may be combined using binary operators.

#: no debug information
DBG_NONE = 0  #: no debug information
#: rql execution information
DBG_RQL = 1
#: executed sql
DBG_SQL = 2
#: repository events
DBG_REPO = 4
#: hooks
DBG_HOOKS = 16
#: operations
DBG_OPS = 32
#: security
DBG_SEC = 64
#: more verbosity
DBG_MORE = 128
#: all level enabled
DBG_ALL = DBG_RQL + DBG_SQL + DBG_REPO + DBG_HOOKS + DBG_OPS + DBG_SEC + DBG_MORE

_SECURITY_ITEMS = []
_SECURITY_CAPS = ["read", "add", "update", "delete", "transition"]

#: current debug mode
DEBUG = 0


@contextmanager
def tunesecurity(items=(), capabilities=()):
    """Context manager to use in conjunction with DBG_SEC.

    This allows some tuning of:
    * the monitored capabilities ('read', 'add', ....)
    * the object being checked by the security checkers

    When no item is given, all of them will be watched.
    By default all capabilities are monitored, unless specified.

    Example use::

      from cubicweb.server import debugged, DBG_SEC, tunesecurity
      with debugged(DBG_SEC):
          with tunesecurity(items=('Elephant', 'trumps'),
                            capabilities=('update', 'delete')):
              babar.cw_set(trumps=celeste)
              flore.cw_delete()

      ==>

      check_perm: 'update' 'relation Elephant.trumps.Elephant'
       [(ERQLExpression(Any X WHERE U has_update_permission X, X eid %(x)s, U eid %(u)s),
       {'eid': 2167}, True)]
      check_perm: 'delete' 'Elephant'
       [(ERQLExpression(Any X WHERE U has_delete_permission X, X eid %(x)s, U eid %(u)s),
       {'eid': 2168}, True)]

    """
    olditems = _SECURITY_ITEMS[:]
    _SECURITY_ITEMS.extend(list(items))
    oldactions = _SECURITY_CAPS[:]
    _SECURITY_CAPS[:] = capabilities
    yield
    _SECURITY_ITEMS[:] = olditems
    _SECURITY_CAPS[:] = oldactions


def set_debug(debugmode):
    """change the repository debugging mode"""
    global DEBUG
    if not debugmode:
        DEBUG = 0
        return
    if isinstance(debugmode, str):
        for mode in splitstrip(debugmode, sep="|"):
            DEBUG |= globals()[mode]
    else:
        DEBUG |= debugmode


class debugged:
    """Context manager and decorator to help debug the repository.

    It can be used either as a context manager:

    >>> with debugged('DBG_RQL | DBG_REPO'):
    ...     # some code in which you want to debug repository activity,
    ...     # seing information about RQL being executed an repository events.

    or as a function decorator:

    >>> @debugged('DBG_RQL | DBG_REPO')
    ... def some_function():
    ...     # some code in which you want to debug repository activity,
    ...     # seing information about RQL being executed an repository events

    The debug mode will be reset to its original value when leaving the "with"
    block or the decorated function.
    """

    def __init__(self, debugmode):
        self.debugmode = debugmode
        self._clevel = None

    def __enter__(self):
        """enter with block"""
        self._clevel = DEBUG
        set_debug(self.debugmode)

    def __exit__(self, exctype, exc, traceback):
        """leave with block"""
        set_debug(self._clevel)
        return traceback is None

    def __call__(self, func):
        """decorate function"""

        def wrapped(*args, **kwargs):
            set_debug(self.debugmode)
            try:
                return func(*args, **kwargs)
            finally:
                set_debug(self._clevel)

        return wrapped


# database initialization ######################################################


def create_user(session, login, pwd, *groups):
    # monkey patch this method if you want to customize admin/anon creation
    # (that maybe necessary if you change CWUser's schema)
    user = session.create_entity("CWUser", login=login, upassword=pwd)
    for group in groups:
        session.execute(
            "SET U in_group G WHERE U eid %(u)s, G name %(group)s",
            {"u": user.eid, "group": group},
        )
    return user


def init_repository(config, interactive=True, drop=False, vreg=None, init_config=None):
    """Initialise a repository database by creating tables and filling them
    with the minimal set of entities (ie at least the schema, base groups and
    a initial user)
    """
    from cubicweb.repoapi import get_repository, connect
    from cubicweb.server.repository import Repository
    from cubicweb.server.utils import manager_userpasswd
    from cubicweb.server.sqlutils import sqlexec, sqlschema, sql_drop_all_user_tables
    from cubicweb.server.sqlutils import (
        _SQL_DROP_ALL_USER_TABLES_FILTER_FUNCTION as drop_filter,
    )

    # configuration to avoid db schema loading and user'state checking
    # on connection
    config.creating = True
    config.consider_user_state = False
    config.cubicweb_appobject_path = set(("hooks", "entities"))
    config.cube_appobject_path = set(("hooks", "entities"))
    # only enable the system source at initialization time
    repo = Repository(config, vreg=vreg)
    repo.bootstrap()
    if init_config is not None:
        # further config initialization once it has been bootstrapped
        init_config(config)
    schema = repo.schema
    sourcescfg = config.read_sources_file()
    source = sourcescfg["system"]
    driver = source["db-driver"]
    with repo.internal_cnx() as cnx:
        sqlcnx = cnx.cnxset.cnx
        sqlcursor = cnx.cnxset.cu
        execute = sqlcursor.execute
        if drop:
            helper = database.get_db_helper(driver)
            dropsql = sql_drop_all_user_tables(helper, sqlcursor)
            # We may fail dropping some tables because of table dependencies, in a first pass.
            # So, we try a second drop sequence to drop remaining tables if needed.
            # Note that 2 passes is an arbitrary choice as it seems enough for our usecases
            # (looping may induce infinite recursion when user have no rights for example).
            # Here we try to keep code simple and backend independent. That's why we don't try to
            # distinguish remaining tables (missing privileges, dependencies, ...).
            failed = sqlexec(
                dropsql, execute, cnx=sqlcnx, pbtitle="-> dropping tables (first pass)"
            )
            if failed:
                failed = sqlexec(
                    failed,
                    execute,
                    cnx=sqlcnx,
                    pbtitle="-> dropping tables (second pass)",
                )
                remainings = list(filter(drop_filter, helper.list_tables(sqlcursor)))
                assert not remainings, f"Remaining tables: {', '.join(remainings)}"
        handler = config.migration_handler(
            schema, interactive=False, repo=repo, cnx=cnx
        )
        # install additional driver specific sql files
        handler.cmd_install_custom_sql_scripts()
        for cube in reversed(config.cubes()):
            handler.cmd_install_custom_sql_scripts(cube)
        _title = "-> creating tables "
        print(_title, end=" ")
        # schema entities and relations tables
        # can't skip entities table even if system source doesn't support them,
        # they are used sometimes by generated sql. Keeping them empty is much
        # simpler than fixing this...
        schemasql = sqlschema(schema, driver)
        failed = sqlexec(schemasql, execute, pbtitle=_title)
        if failed:
            print("The following SQL statements failed. You should check your schema.")
            print(failed)
            raise Exception(
                "execution of the sql schema failed, you should check your schema"
            )
        sqlcnx.commit()
    with repo.internal_cnx() as cnx:
        # insert entity representing the system source
        ssource = cnx.create_entity("CWSource", type="native", name="system")
        repo.system_source.eid = ssource.eid
        cnx.execute("SET X cw_source X WHERE X eid %(x)s", {"x": ssource.eid})
        # insert base groups and default admin
        print("-> inserting default user and default groups.")
        try:
            login = sourcescfg["admin"]["login"]
            pwd = sourcescfg["admin"]["password"]
        except KeyError:
            if interactive:
                msg = "enter login and password of the initial manager account"
                login, pwd = manager_userpasswd(msg=msg, confirm=True)
            else:
                login, pwd = source["db-user"], source["db-password"]
        # sort for eid predicatability as expected in some server tests
        for group in sorted(BASE_GROUPS):
            cnx.create_entity("CWGroup", name=group)
        admin = create_user(cnx, login, pwd, "managers")
        cnx.execute(
            "SET X owned_by U WHERE X is IN (CWGroup,CWSource), U eid %(u)s",
            {"u": admin.eid},
        )
        cnx.commit()
    repo.shutdown()
    # re-login using the admin user
    config._cubes = None  # avoid assertion error
    repo = get_repository(config=config)
    # replace previous schema by the new repo's one. This is necessary so that we give the proper
    # schema to `initialize_schema` above since it will initialize .eid attribute of schema elements
    schema = repo.schema
    with connect(repo, login, password=pwd) as cnx:
        with cnx.security_enabled(False, False):
            repo.system_source.eid = ssource.eid  # redo this manually
            handler = config.migration_handler(
                schema, interactive=False, cnx=cnx, repo=repo
            )
            # serialize the schema
            initialize_schema(config, schema, handler)
            # yoo !
            cnx.commit()
            repo.system_source.init_creating()
            cnx.commit()
    repo.shutdown()
    # restore initial configuration
    config.creating = False
    config.consider_user_state = True
    # (drop instance attribute to get back to class attribute)
    del config.cubicweb_appobject_path
    del config.cube_appobject_path
    print(f"-> database for instance {config.appid} initialized.")


def initialize_schema(config, schema, mhandler, event="create"):
    from cubicweb.server.schemaserial import serialize_schema

    cnx = mhandler.cnx
    cubes = config.cubes()
    # deactivate every hooks but those responsible to set metadata
    # so, NO INTEGRITY CHECKS are done, to have quicker db creation.
    # Active integrity is kept else we may pb such as two default
    # workflows for one entity type.
    with cnx.deny_all_hooks_but("metadata", "activeintegrity"):
        # execute cubicweb's pre<event> script
        mhandler.cmd_exec_event_script(f"pre{event}")
        # execute cubes pre<event> script if any
        for cube in reversed(cubes):
            mhandler.cmd_exec_event_script(f"pre{event}", cube)
        # execute instance's pre<event> script (useful in tests)
        mhandler.cmd_exec_event_script(f"pre{event}", apphome=True)
        # enter instance'schema into the database
        serialize_schema(cnx, schema)
        cnx.commit()
        # execute cubicweb's post<event> script
        mhandler.cmd_exec_event_script(f"post{event}")
        # execute cubes'post<event> script if any
        for cube in reversed(cubes):
            mhandler.cmd_exec_event_script(f"post{event}", cube)
        # execute instance's post<event> script (useful in tests)
        mhandler.cmd_exec_event_script(f"post{event}", apphome=True)


# sqlite'stored procedures have to be registered at connection opening time

# add to this set relations which should have their add security checking done
# *BEFORE* adding the actual relation (done after by default)
BEFORE_ADD_RELATIONS = set(("owned_by",))

# add to this set relations which should have their add security checking done
# *at COMMIT TIME* (done after by default)
ON_COMMIT_ADD_RELATIONS = set(())

# available sources registry
SOURCE_TYPES = {
    "native": LazyObject("cubicweb.server.sources.native", "NativeSQLSource"),
    "datafeed": LazyObject("cubicweb.server.sources.datafeed", "DataFeedSource"),
    "ldapfeed": LazyObject("cubicweb.server.sources.ldapfeed", "LDAPFeedSource"),
}
