#       Licensed to the Apache Software Foundation (ASF) under one
#       or more contributor license agreements.  See the NOTICE file
#       distributed with this work for additional information
#       regarding copyright ownership.  The ASF licenses this file
#       to you under the Apache License, Version 2.0 (the
#       "License"); you may not use this file except in compliance
#       with the License.  You may obtain a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#       Unless required by applicable law or agreed to in writing,
#       software distributed under the License is distributed on an
#       "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
#       KIND, either express or implied.  See the License for the
#       specific language governing permissions and limitations
#       under the License.

"""Setup the allura application"""
import os
import logging
from datetime import datetime
from textwrap import dedent

import tg
from tg import tmpl_context as c, app_globals as g
from paste.deploy.converters import asbool
import ew

from allura.model.oauth import dummy_oauths
from ming import Session, mim
from ming.odm import state, session
from ming.odm.odmsession import ThreadLocalODMSession

import allura
from allura.lib import plugin
from allura.lib import helpers as h
from allura import model as M
from allura.command import EnsureIndexCommand
from allura.command import CreateTroveCategoriesCommand
from allura.websetup.schema import REGISTRY

from forgewiki import model as WM

log = logging.getLogger(__name__)


def bootstrap(command, conf, vars):
    """Place any commands to setup allura here"""
    # are we being called by the test suite?
    test_run = conf.get('__file__', '').endswith('test.ini')

    if not test_run:
        # when this is run via the `setup-app` cmd, some macro rendering needs this set up
        REGISTRY.register(ew.widget_context,
                          ew.core.WidgetContext('http', ew.ResourceManager()))

    create_test_data = asbool(os.getenv('ALLURA_TEST_DATA', 'true'))

    # if this is a test_run, skip user project creation to save time
    make_user_projects = not test_run

    def make_user(*args, **kw):
        kw.update(make_project=make_user_projects)
        return create_user(*args, **kw)

    # Temporarily disable auth extensions to prevent unintended side-effects
    tg.config['auth.method'] = tg.config['registration.method'] = 'local'
    assert tg.config['auth.method'] == 'local'
    conf['auth.method'] = conf['registration.method'] = 'local'

    # Clean up all old stuff
    ThreadLocalODMSession.close_all()
    c.user = c.project = c.app = None
    wipe_database()
    try:
        g.solr.delete(q='*:*')
    except Exception:  # pragma no cover
        log.error('SOLR server is %s', g.solr_server)
        log.error('Error clearing solr index')

    # set up mongo indexes
    index = EnsureIndexCommand('ensure_index')
    index.run([''])

    log.info('Registering root user & default neighborhoods')
    anon = M.User.anonymous_uncached()
    session(M.User).save(anon)

    # never make a user project for the root user
    if create_test_data:
        root = create_user('Root', make_project=False)
    else:
        from getpass import getpass
        root_name = input('Enter username for root user (default "root"): ').strip()
        if not root_name:
            root_name = 'root'
        ok = False
        while not ok:
            root_password1 = getpass('Enter password: ')
            if len(root_password1) == 0:
                log.info('Password must not be empty')
                continue
            root_password2 = getpass('Confirm password: ')
            if root_password1 != root_password2:
                log.info("Passwords don't match")
                continue
            root = create_user(root_name, password=root_password1, make_project=False)
            ok = True

    n_projects = M.Neighborhood(name='Projects', url_prefix='/p/',
                                features=dict(private_projects=True,
                                              max_projects=None,
                                              css='none',
                                              google_analytics=False))
    n_users = M.Neighborhood(name='Users', url_prefix='/u/',
                             shortname_prefix='u/',
                             anchored_tools='profile:Profile,userstats:Statistics',
                             features=dict(private_projects=True,
                                           max_projects=None,
                                           css='none',
                                           google_analytics=False))

    assert tg.config['auth.method'] == 'local'
    project_reg = plugin.ProjectRegistrationProvider.get()
    p_projects = project_reg.register_neighborhood_project(
        n_projects, [root], allow_register=True)
    p_users = project_reg.register_neighborhood_project(n_users, [root])

    def set_nbhd_wiki_content(nbhd_proj, content):
        wiki = nbhd_proj.app_instance('wiki')
        page = WM.Page.query.get(
            app_config_id=wiki.config._id, title=wiki.root_page_name)
        page.text = content

    set_nbhd_wiki_content(p_projects, dedent('''
        Welcome to the "Projects" neighborhood.  It is the default neighborhood in Allura.
        You can edit this wiki page as you see fit.  Here's a few ways to get started:

        [Register a new project](/p/add_project)

        [Neighborhood administration](/p/admin)

        [[projects show_total=yes]]
        '''))
    set_nbhd_wiki_content(p_users, dedent('''
        This is the "Users" neighborhood.
        All users automatically get a user-project created for them, using their username.

        [Neighborhood administration](/u/admin)

        [[projects show_total=yes]]
        '''))
    if create_test_data:
        n_adobe = M.Neighborhood(
            name='Adobe', url_prefix='/adobe/', project_list_url='/adobe/',
            features=dict(private_projects=True,
                          max_projects=None,
                          css='custom',
                          google_analytics=True))
        p_adobe = project_reg.register_neighborhood_project(n_adobe, [root])
        set_nbhd_wiki_content(p_adobe, dedent('''
            This is the "Adobe" neighborhood.  It is just an example of having projects in a different neighborhood.

            [Neighborhood administration](/adobe/admin)

            [[projects show_total=yes]]
            '''))
        # add the adobe icon
        file_name = 'adobe_icon.png'
        file_path = os.path.join(
            allura.__path__[0], 'public', 'nf', 'images', file_name)
        M.NeighborhoodFile.from_path(file_path, neighborhood_id=n_adobe._id)

    ThreadLocalODMSession.flush_all()
    ThreadLocalODMSession.close_all()

    if create_test_data:
        # Add some test users
        for unum in range(4):
            make_user('Test User %d' % unum)

    log.info('Creating basic project categories')
    M.ProjectCategory(name='clustering', label='Clustering')
    cat2 = M.ProjectCategory(name='communications', label='Communications')
    M.ProjectCategory(
        name='synchronization', label='Synchronization', parent_id=cat2._id)
    M.ProjectCategory(
        name='streaming', label='Streaming', parent_id=cat2._id)
    M.ProjectCategory(name='fax', label='Fax', parent_id=cat2._id)
    M.ProjectCategory(name='bbs', label='BBS', parent_id=cat2._id)

    cat3 = M.ProjectCategory(name='database', label='Database')
    M.ProjectCategory(
        name='front_ends', label='Front-Ends', parent_id=cat3._id)
    M.ProjectCategory(
        name='engines_servers', label='Engines/Servers', parent_id=cat3._id)

    if create_test_data:
        log.info('Registering "regular users" (non-root) and default projects')
        # since this runs a lot for tests, separate test and default users and
        # do the minimal needed
        if asbool(conf.get('load_test_data')):
            u_admin = make_user('Test Admin')
            u_admin.preferences = dict(email_address='test-admin@users.localhost')
            u_admin.email_addresses = ['test-admin@users.localhost']
            fast_set_pwd(u_admin, 'foo')
            u_admin.claim_address('test-admin@users.localhost')
            ThreadLocalODMSession.flush_all()

            admin_email = M.EmailAddress.get(email='test-admin@users.localhost')
            admin_email.confirmed = True
            admin_email.confirmed_date = datetime.utcnow()
        else:
            u_admin = make_user('Admin 1', username='admin1')
            # Admin1 is almost root, with admin access for Users and Projects
            # neighborhoods
            p_projects.add_user(u_admin, ['Admin'])
            p_users.add_user(u_admin, ['Admin'])

            n_projects.register_project('allura', u_admin, 'Allura')
        make_user('Test User')
        n_adobe.register_project('adobe-1', u_admin, 'Adobe project 1')
        p_adobe.add_user(u_admin, ['Admin'])
        p0 = n_projects.register_project('test', u_admin, 'Test Project')
        n_projects.register_project('test2', u_admin, 'Test 2')
        p0._extra_tool_status = ['alpha', 'beta']

    sess = session(M.Neighborhood)  # all the sessions are the same
    _list = (n_projects, n_users, p_projects, p_users)
    if create_test_data:
        _list += (n_adobe, p_adobe)
    for x in _list:
        # Ming doesn't detect substructural changes in newly created objects
        # (vs loaded from DB)
        state(x).status = 'dirty'
        # TODO: Hope that Ming can be improved to at least avoid stuff below
        sess.flush(x)

    ThreadLocalODMSession.flush_all()

    if not asbool(conf.get('load_test_data')):
        # regular first-time setup

        create_trove_categories = CreateTroveCategoriesCommand('create_trove_categories')
        create_trove_categories.run([''])

        if create_test_data:
            p0.add_user(u_admin, ['Admin'])
            log.info('Registering initial apps')
            with h.push_config(c, user=u_admin):
                p0.install_apps([{'ep_name': ep_name}
                                 for ep_name, app in g.entry_points['tool'].items()
                                 if app._installable(tool_name=ep_name,
                                                     nbhd=n_projects,
                                                     project_tools=[])
                                 ])

    ThreadLocalODMSession.flush_all()
    ThreadLocalODMSession.close_all()

    if create_test_data:
        # reload our p0 project so that p0.app_configs is accurate with all the
        # newly installed apps
        p0 = M.Project.query.get(_id=p0._id)
        sub = p0.new_subproject('sub1', project_name='A Subproject')
        with h.push_config(c, user=u_admin):
            sub.install_app('wiki')

    if not test_run:
        # only when running setup-app do we need this.  the few tests that need it do it themselves
        dummy_oauths()

    ThreadLocalODMSession.flush_all()
    ThreadLocalODMSession.close_all()


def wipe_database():
    conn = M.main_doc_session.bind.conn
    if isinstance(conn, mim.Connection):
        clear_all_database_tables()
        for db in conn.list_database_names():
            db = conn[db]
    else:
        for database in conn.list_database_names():
            if database not in ('allura', 'pyforge', 'project-data'):
                continue
            log.info('Wiping database %s', database)
            db = conn[database]
            for coll in list(db.list_collection_names()):
                if coll.startswith('system.'):
                    continue
                log.info('Dropping collection %s:%s', database, coll)
                try:
                    db.drop_collection(coll)
                except Exception:
                    pass


def clear_all_database_tables():
    conn = M.main_doc_session.bind.conn
    for db in conn.list_database_names():
        db = conn[db]
        for coll in list(db.list_collection_names()):
            if coll == 'system.indexes':
                continue
            db.drop_collection(coll)


# this re-uses hashes for the same pwd, which gives a huge speedup during tests.  Not good for real usage, since salting is the same.
user_pwd_hash_speedup_cache = {}


def fast_set_pwd(user: M.User, password: str):
    if password in user_pwd_hash_speedup_cache:
        user.password, user.password_algorithm = user_pwd_hash_speedup_cache[password]
        user.last_password_updated = datetime.utcnow()
    else:
        user.set_password(password)
        user_pwd_hash_speedup_cache[password] = (user.password, user.password_algorithm)


def create_user(display_name, username=None, password='foo', make_project=False):  # noqa: S107
    if not username:
        username = display_name.lower().replace(' ', '-')
    user = M.User.register(dict(username=username,
                                display_name=display_name),
                           make_project=make_project)
    email = username+"@allura.local"
    user.claim_address(email)
    from allura.model.auth import EmailAddress
    kw = {"email": email}
    em = EmailAddress.get(**kw)
    em.confirmed = True
    em.confirmed_date = datetime.utcnow()
    em.set_nonce_hash()
    user.set_pref('email_address', email)
    fast_set_pwd(user, password)
    return user


class DBSession(Session):

    '''Simple session that takes a pymongo connection and a database name'''

    def __init__(self, db):
        self._db = db

    @property
    def db(self):
        return self._db

    def _impl(self, cls):
        return self.db[cls.__mongometa__.name]
