#       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.
import re
from collections import OrderedDict

from tg import expose, flash, redirect, validate, config
from tg import tmpl_context as c
from tg.decorators import without_trailing_slash
from webob.exc import HTTPForbidden, HTTPNotFound
from tg import app_globals as g

from allura import model as M
from allura.controllers import BaseController
from allura.lib import helpers as h
from allura.lib.decorators import require_post
from allura.lib.security import require_authenticated, require_site_admin
from allura.lib.widgets import forms
from allura.lib.plugin import SiteAdminExtension
from allura.app import SitemapEntry


class F:
    remove_category_form = forms.RemoveTroveCategoryForm()
    add_category_form = forms.AddTroveCategoryForm()


class TroveAdminException(Exception):
    def __init__(self, flash_args, redir_params='', upper=None):
        super().__init__()

        self.flash_args = flash_args
        self.redir_params = redir_params
        self.upper = upper


class TroveCategoryController(BaseController):
    @expose()
    def _lookup(self, trove_cat_id, *remainder):
        cat = M.TroveCategory.query.get(trove_cat_id=int(trove_cat_id))
        if not cat:
            raise HTTPNotFound
        return TroveCategoryController(category=cat), remainder

    def _check_security(self):
        require_authenticated()

        enable_editing = config.get('trovecategories.enableediting', 'false')
        if enable_editing == 'admin':
            require_site_admin(c.user)
        elif enable_editing != 'true':
            raise HTTPForbidden()

    def __init__(self, category=None):
        self.category = category
        super().__init__()

    @expose('jinja:allura:templates/trovecategories.html')
    def index(self, **kw):
        if self.category:
            selected_cat = self.category
            l = self.category.subcategories
            hierarchy = []
            temp_cat = self.category.parent_category
            while temp_cat:
                hierarchy = [temp_cat] + hierarchy
                temp_cat = temp_cat.parent_category
        else:
            l = M.TroveCategory.query.find(dict(trove_parent_id=0)).sort('fullname').all()
            selected_cat = None
            hierarchy = []
        return dict(
            categories=l,
            selected_cat=selected_cat,
            hierarchy=hierarchy,
            kw=kw)

    def generate_category(self, category):
        if not category:
            return ()

        children = {
            key: value
            for (key, value) in
            (self.generate_category(child) for child in category.subcategories)
        }

        return category.fullname, OrderedDict(sorted(children.items()))

    @without_trailing_slash
    @expose('jinja:allura:templates/browse_trove_categories.html')
    def browse(self):
        parent_categories = M.TroveCategory.query.find(dict(trove_parent_id=0)).all()
        tree = {
            key: value
            for (key, value) in
            (self.generate_category(child) for child in parent_categories)
        }
        return dict(tree=OrderedDict(sorted(tree.items())))

    @classmethod
    def _create(cls, name, upper_id, shortname):

        upper = M.TroveCategory.query.get(trove_cat_id=upper_id)
        if upper_id == 0:
            path = name
            show_as_skill = True
        elif upper is None:
            raise TroveAdminException(('Invalid upper category.', "error"))
        else:
            path = upper.fullpath + " :: " + name
            show_as_skill = upper.show_as_skill

        newid = max(
            el.trove_cat_id for el in M.TroveCategory.query.find()) + 1
        shortname = h.slugify(shortname or name, True)[1]

        if upper:
            trove_type = upper.fullpath.split(' :: ')[0]
            fullpath_re = fr'^{re.escape(trove_type)} :: '  # e.g. scope within "Topic :: "
        else:
            # no parent, so making a top-level.  Don't limit fullpath_re, so enforcing global uniqueness
            fullpath_re = r''
        oldcat = M.TroveCategory.query.get(shortname=shortname, fullpath={'$regex': fullpath_re})

        if oldcat:
            raise TroveAdminException(
                (f'A category with shortname "{shortname}" already exists ({oldcat.fullpath}).  Try a different, unique shortname', "error"),
                f'?categoryname={name}&shortname={shortname}',
                upper
            )
        else:
            M.TroveCategory(
                trove_cat_id=newid,
                trove_parent_id=upper_id,
                fullname=name,
                shortname=shortname,
                fullpath=path,
                show_as_skill=show_as_skill)
            return upper, ('Category "%s" successfully created.' % name,), ''

    @expose()
    @require_post()
    @validate(F.add_category_form, error_handler=index)
    def create(self, **kw):
        name = kw.get('categoryname')
        upper_id = int(kw.get('uppercategory_id', 0))
        shortname = kw.get('shortname', None)

        try:
            upper, flash_args, redir_params = self._create(name, upper_id, shortname)
        except TroveAdminException as ex:
            upper = ex.upper
            flash_args = ex.flash_args
            redir_params = ex.redir_params

        flash(*flash_args)

        if upper:
            redirect(f'/categories/{upper.trove_cat_id}/{redir_params}')
        else:
            redirect(f'/categories/{redir_params}')

    @expose()
    @require_post()
    @validate(F.remove_category_form, error_handler=index)
    def remove(self, **kw):
        cat = M.TroveCategory.query.get(trove_cat_id=int(kw['categoryid']))
        if cat.trove_parent_id:
            parent = M.TroveCategory.query.get(
                trove_cat_id=cat.trove_parent_id)
            redirecturl = '/categories/%s' % parent.trove_cat_id
        else:
            redirecturl = '/categories'
        if len(cat.subcategories) > 0:
            m = "This category contains at least one sub-category, "
            m = m + "therefore it can't be removed."
            flash(m, "error")
            redirect(redirecturl)
            return

        if M.User.withskill(cat).count() > 0:
            m = "This category is used as a skill by at least a user, "
            m = m + "therefore it can't be removed."
            flash(m, "error")
            redirect(redirecturl)
            return

        if M.Project.query.get(trove_root_database=cat._id):
            m = "This category is used as a database by at least a project, "
            m = m + "therefore it can't be removed."
            flash(m, "error")
            redirect(redirecturl)
            return

        if M.Project.query.get(trove_developmentstatus=cat._id):
            m = "This category is used as development status by at least a "
            m = m + "project, therefore it can't be removed."
            flash(m, "error")
            redirect(redirecturl)
            return

        if M.Project.query.get(trove_audience=cat._id):
            m = "This category is used as intended audience by at least a "
            m = m + "project, therefore it can't be removed."
            flash(m, "error")
            redirect(redirecturl)
            return

        if M.Project.query.get(trove_license=cat._id):
            m = "This category is used as a license by at least a "
            m = m + "project, therefore it can't be removed."
            flash(m, "error")
            redirect(redirecturl)
            return

        if M.Project.query.get(trove_os=cat._id):
            m = "This category is used as operating system by at least a "
            m = m + "project, therefore it can't be removed."
            flash(m, "error")
            redirect(redirecturl)
            return

        if M.Project.query.get(trove_language=cat._id):
            m = "This category is used as programming language by at least a "
            m = m + "project, therefore it can't be removed."
            flash(m, "error")
            redirect(redirecturl)
            return

        if M.Project.query.get(trove_topic=cat._id):
            m = "This category is used as a topic by at least a "
            m = m + "project, therefore it can't be removed."
            flash(m, "error")
            redirect(redirecturl)
            return

        if M.Project.query.get(trove_natlanguage=cat._id):
            m = "This category is used as a natural language by at least a "
            m = m + "project, therefore it can't be removed."
            flash(m, "error")
            redirect(redirecturl)
            return

        if M.Project.query.get(trove_environment=cat._id):
            m = "This category is used as an environment by at least a "
            m = m + "project, therefore it can't be removed."
            flash(m, "error")
            redirect(redirecturl)
            return

        M.TroveCategory.delete(cat)

        flash('Category removed.')
        redirect(redirecturl)


class TroveCategorySiteAdminExtension(SiteAdminExtension):
    def update_sidebar_menu(self, links):
        enable_editing = config.get('trovecategories.enableediting', 'false')
        if enable_editing in ('admin', 'true'):
            links.append(SitemapEntry('Troves', '/categories',
                                      ui_icon=g.icons['admin']))
