#       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.

from io import BytesIO

import datetime
import string
import logging
try:
    from typing import Union
except ImportError:  # py2 doesn't have typing yet
    pass

import colander as col
import bson
import requests
import formencode
from urllib.parse import urlparse

from allura.lib.helpers import slugify
from ming.base import Object
from ming.odm import ThreadLocalODMSession
from tg import tmpl_context as c, app_globals as g

from allura import model as M
from allura.lib import helpers as h
from allura.lib.plugin import ProjectRegistrationProvider


log = logging.getLogger(__name__)


"""
Validators and helpers for adding projects via the API or project-import.py script
"""


class TroveCategory():

    def __init__(self, root_type=''):
        self.root_type = root_type

    def deserialize(self, node, cstruct):
        if cstruct is col.null:
            return col.null
        cat = M.TroveCategory.query.get(fullpath=cstruct)
        if not cat:
            cat = M.TroveCategory.query.get(fullname=cstruct)
        if not cat:
            raise col.Invalid(node,
                              '"%s" is not a valid trove category.' % cstruct)
        if not cat.fullpath.startswith(self.root_type):
            raise col.Invalid(node,
                              '"%s" is not a valid "%s" trove category.' %
                              (cstruct, self.root_type))
        return cat


class User():

    def deserialize(self, node, cstruct):
        if cstruct is col.null:
            return col.null
        user = M.User.by_username(cstruct)
        if not user:
            raise col.Invalid(node,
                              'Invalid username "%s".' % cstruct)
        return user


class ProjectShortnameType():

    def __init__(self, nbhd, update):
        self.nbhd = nbhd
        self.update = update

    def deserialize(self, node, cstruct):
        if cstruct is col.null:
            return col.null
        return ProjectRegistrationProvider.get().shortname_validator.to_python(cstruct,
                                                                               check_allowed=not self.update,
                                                                               neighborhood=self.nbhd)


class Award():

    def __init__(self, nbhd):
        self.nbhd = nbhd

    def deserialize(self, node, cstruct):
        if cstruct is col.null:
            return col.null
        award = M.Award.query.find(dict(short=cstruct,
                                        created_by_neighborhood_id=self.nbhd._id)).first()
        if not award:
            # try to look up the award by _id
            award = M.Award.query.find(dict(_id=bson.ObjectId(cstruct),
                                            created_by_neighborhood_id=self.nbhd._id)).first()
        if not award:
            raise col.Invalid(node,
                              'Invalid award "%s".' % cstruct)
        return award


class TroveTopics(col.SequenceSchema):
    trove_topics = col.SchemaNode(TroveCategory("Topic"))


class TroveLicenses(col.SequenceSchema):
    trove_license = col.SchemaNode(TroveCategory("License"))


class TroveDatabases(col.SequenceSchema):
    trove_databases = col.SchemaNode(TroveCategory("Database Environment"))


class TroveStatuses(col.SequenceSchema):
    trove_statuses = col.SchemaNode(TroveCategory("Development Status"))


class TroveAudiences(col.SequenceSchema):
    trove_audience = col.SchemaNode(TroveCategory("Intended Audience"))


class TroveOSes(col.SequenceSchema):
    trove_oses = col.SchemaNode(TroveCategory("Operating System"))


class TroveLanguages(col.SequenceSchema):
    trove_languages = col.SchemaNode(TroveCategory("Programming Language"))


class TroveTranslations(col.SequenceSchema):
    trove_translations = col.SchemaNode(TroveCategory("Translations"))


class TroveUIs(col.SequenceSchema):
    trove_uis = col.SchemaNode(TroveCategory("User Interface"))


class Labels(col.SequenceSchema):
    label = col.SchemaNode(col.Str())


class NewProjectSchema(col.MappingSchema):
    def schema_type(self, **kw):
        return col.Mapping(unknown='raise')

    name = col.SchemaNode(col.Str())
    summary = col.SchemaNode(col.Str(), missing='')
    description = col.SchemaNode(col.Str(), missing='')
    admin = col.SchemaNode(User())
    private = col.SchemaNode(col.Bool(), missing=False)
    labels = Labels(missing=[])
    external_homepage = col.SchemaNode(col.Str(), missing='')
    video_url = col.SchemaNode(col.Str(), missing='')
    trove_root_databases = TroveDatabases(missing=None)
    trove_developmentstatuses = TroveStatuses(validator=col.Length(max=6), missing=None)
    trove_audiences = TroveAudiences(validator=col.Length(max=6), missing=None)
    trove_licenses = TroveLicenses(validator=col.Length(max=6), missing=None)
    trove_oses = TroveOSes(missing=None)
    trove_languages = TroveLanguages(validator=col.Length(max=6), missing=None)
    trove_topics = TroveTopics(validator=col.Length(max=3), missing=None)
    trove_natlanguages = TroveTranslations(missing=None)
    trove_environments = TroveUIs(missing=None)
    tool_data = col.SchemaNode(col.Mapping(unknown='preserve'), missing={})
    icon_url = col.SchemaNode(col.Str(), missing=None, validator=col.url)
    # more fields are added dynamically to the schema in make_newproject_schema()


def trove_ids(orig, new_):
    if new_ is None:
        return orig
    return list({t._id for t in list(new_)})


def make_newproject_schema(nbhd, update=False):
    # type: (Neighborhood, bool) -> NewProjectSchema
    projectSchema = NewProjectSchema(unknown='raise')
    # dynamically add to the schema fields that depend on `nbhd`
    projectSchema.add(col.SchemaNode(col.Sequence(),
                                     col.SchemaNode(Award(nbhd)),
                                     name='awards', missing=[]))
    projectSchema.add(col.SchemaNode(ProjectShortnameType(nbhd, update),
                                     name='shortname', missing=None))
    return projectSchema


def deserialize_project(datum, projectSchema, nbhd):
    # type: (dict, NewProjectSchema, Neighborhood) -> object
    p = projectSchema.deserialize(datum)
    p = Object(p)  # convert from dict to something with attr-access

    # generate a shortname, and try to make it unique
    if not p.shortname:
        max_shortname_len = 15  # maybe more depending on NeighborhoodProjectShortNameValidator impl, but this is safe
        shortname = orig_shortname = make_shortname(p.name, max_shortname_len)
        for i in range(1, 10):
            try:
                ProjectRegistrationProvider.get().shortname_validator.to_python(shortname, neighborhood=nbhd)
            except formencode.api.Invalid:
                if len(orig_shortname) == max_shortname_len - 1:
                    shortname = orig_shortname + str(i)
                else:
                    shortname = orig_shortname[:max_shortname_len - 1] + str(i)
            else:
                # we're good!
                break
        p.shortname = shortname

    return p


def make_shortname(name, max_len):
    # lowercase, drop periods and underscores
    shortname = slugify(name)[1].replace('_', '-')
    # must start with a letter
    if not shortname[0].isalpha():
        shortname = 'a-' + shortname
    # truncate length, avoid trailing dash
    shortname = shortname[:max_len].rstrip('-')
    # too short
    if len(shortname) < 3:
        shortname += '-z'
    return shortname


def create_project_with_attrs(p, nbhd, update=False, ensure_tools=False):
    # type: (object, M.Neighborhood, bool, bool) -> Union[M.Project|bool]
    M.session.artifact_orm_session._get().skip_mod_date = True
    shortname = p.shortname
    project = M.Project.query.get(shortname=shortname,
                                  neighborhood_id=nbhd._id)
    project_template = nbhd.get_project_template()

    if project and not update:
        log.warning('Skipping existing project "%s"' % (shortname))
        return False

    if not project:
        creating = True
        project = nbhd.register_project(shortname,
                                        p.admin,
                                        project_name=p.name,
                                        private_project=p.private,
                                        omit_event=True,  # because we'll fire it later after setting other fields
                                        )
    else:
        creating = False
        log.info('Updating project "%s".' % (shortname))

    project.notifications_disabled = True

    if ensure_tools and 'tools' in project_template:
        for i, tool in enumerate(project_template['tools'].keys()):
            tool_config = project_template['tools'][tool]
            if project.app_instance(tool_config['mount_point']):
                continue
            tool_options = tool_config.get('options', {})
            for k, v in list(tool_options.items()):
                if isinstance(v, str):
                    tool_options[k] = string.Template(v).safe_substitute(
                        project.root_project.__dict__.get('root_project', {}))
            project.install_app(tool,
                                mount_label=tool_config['label'],
                                mount_point=tool_config['mount_point'],
                                **tool_options)

    project.summary = p.summary
    project.short_description = p.description
    project.external_homepage = p.external_homepage
    project.video_url = p.video_url
    project.last_updated = datetime.datetime.utcnow()
    # These properties may have been populated by nbhd template defaults in
    # register_project(). Overwrite if we have data, otherwise keep defaults.
    project.labels = p.labels or project.labels
    project.trove_root_database = trove_ids(project.trove_root_database, p.trove_root_databases)
    project.trove_developmentstatus = trove_ids(project.trove_developmentstatus, p.trove_developmentstatuses)
    project.trove_audience = trove_ids(project.trove_audience, p.trove_audiences)
    project.trove_license = trove_ids(project.trove_license, p.trove_licenses)
    project.trove_os = trove_ids(project.trove_os, p.trove_oses)
    project.trove_language = trove_ids(project.trove_language, p.trove_languages)
    project.trove_topic = trove_ids(project.trove_topic, p.trove_topics)
    project.trove_natlanguage = trove_ids(project.trove_natlanguage, p.trove_natlanguages)
    project.trove_environment = trove_ids(project.trove_environment, p.trove_environments)

    project.tool_data.update(p.tool_data)

    for a in p.awards:
        M.AwardGrant(app_config_id=bson.ObjectId(),
                     award_id=a._id,
                     granted_to_project_id=project._id,
                     granted_by_neighborhood_id=nbhd._id)

    if p.icon_url:
        req = requests.get(p.icon_url, timeout=30)
        req.raise_for_status()
        project.save_icon(urlparse(p.icon_url).path,
                          BytesIO(req.content),
                          content_type=req.headers.get('Content-Type'))
    elif getattr(p, 'icon', None):
        with open(p.icon, 'rb') as icon_file:
            project.save_icon(p.icon, icon_file)

    project.notifications_disabled = False
    with h.push_config(c, project=project, user=p.admin):
        ThreadLocalODMSession.flush_all()
        if creating:
            g.post_event('project_created')
        else:
            g.post_event('project_updated', __task_delay=5)
    return project
