# Copyright 2017 The Armada Authors.
#
# Licensed 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 copy import deepcopy

from oslo_log import log as logging

from armada import const
from armada import exceptions
from armada.handlers import schema

LOG = logging.getLogger(__name__)


class Manifest(object):
    def __init__(self, documents, target_manifest=None):
        """Instantiates a Manifest object.

        An Armada Manifest expects that at least one of each of the following
        be included in ``documents``:

        * A document with schema "armada/Chart/v1"
        * A document with schema "armada/ChartGroup/v1"

        And only one document of the following is allowed:

        * A document with schema "armada/Manifest/v1"

        If multiple documents with schema "armada/Manifest/v1" are provided,
        specify ``target_manifest`` to select the target one.

        :param List[dict] documents: Documents out of which to build the
            Armada Manifest.
        :param str target_manifest: The target manifest to use when multiple
            documents with "armada/Manifest/v1" are contained in
            ``documents``. Default is None.
        :raises ManifestException: If the expected number of document types
            are not found or if the document types are missing required
            properties.
        """
        self.documents = deepcopy(documents)
        self.charts, self.groups, manifests = self._find_documents(
            target_manifest)

        if len(manifests) > 1:
            error = (
                'Multiple manifests are not supported. Ensure that the '
                '`target_manifest` option is set to specify the target '
                'manifest')
            LOG.error(error)
            raise exceptions.ManifestException(details=error)
        else:
            self.manifest = manifests[0] if manifests else None

        if not all([self.charts, self.groups, self.manifest]):
            expected_schemas = [schema.TYPE_CHART, schema.TYPE_CHARTGROUP]
            error = (
                'Documents must include at least one of each of {} '
                'and only one {}').format(
                    expected_schemas, schema.TYPE_MANIFEST)
            LOG.error(error)
            raise exceptions.ManifestException(details=error)

    def _find_documents(self, target_manifest=None):
        """Returns the chart documents, chart group documents,
        and Armada manifest

        If multiple documents with schema "armada/Manifest/v1" are provided,
        specify ``target_manifest`` to select the target one.

        :param str target_manifest: The target manifest to use when multiple
            documents with "armada/Manifest/v1" are contained in
            ``documents``. Default is None.
        :returns: Tuple of chart documents, chart groups, and manifests
            found in ``self.documents``
        :rtype: tuple
        """
        charts = []
        groups = []
        manifests = []
        for document in self.documents:
            schema_info = schema.get_schema_info(document.get('schema'))
            if not schema_info:
                continue
            if schema_info.type == schema.TYPE_CHART:
                charts.append(document)
            if schema_info.type == schema.TYPE_CHARTGROUP:
                groups.append(document)
            if schema_info.type == schema.TYPE_MANIFEST:
                manifest_name = document.get('metadata', {}).get('name')
                if target_manifest:
                    if manifest_name == target_manifest:
                        manifests.append(document)
                else:
                    manifests.append(document)
        return charts, groups, manifests

    def find_chart_document(self, name):
        """Returns a chart document with the specified name

        :param str name: name of the desired chart document
        :returns: The requested chart document
        :rtype: dict
        :raises ManifestException: If a chart document with the
            specified name is not found
        """
        for chart in self.charts:
            if chart.get('metadata', {}).get('name') == name:
                return chart
        raise exceptions.BuildChartException(
            details='Could not find {} named "{}"'.format(
                schema.TYPE_CHART, name))

    def find_chart_group_document(self, name):
        """Returns a chart group document with the specified name

        :param str name: name of the desired chart group document
        :returns: The requested chart group document
        :rtype: dict
        :raises ManifestException: If a chart
            group document with the specified name is not found
        """
        for group in self.groups:
            if group.get('metadata', {}).get('name') == name:
                return group
        raise exceptions.BuildChartGroupException(
            details='Could not find {} named "{}"'.format(
                schema.TYPE_CHARTGROUP, name))

    def build_chart_deps(self, chart):
        """Recursively build chart dependencies for ``chart``.

        :param dict chart: The chart whose dependencies will be recursively
            built.
        :returns: The chart with all dependencies.
        :rtype: dict
        :raises ManifestException: If a chart for a dependency name listed
            under ``chart['data']['dependencies']`` could not be found.
        """
        try:
            chart_dependencies = chart.get(const.KEYWORD_DATA,
                                           {}).get('dependencies', [])
            for iter, dep in enumerate(chart_dependencies):
                if isinstance(dep, dict):
                    continue
                chart_dep = self.find_chart_document(dep)
                self.build_chart_deps(chart_dep)
                chart[const.KEYWORD_DATA]['dependencies'][iter] = chart_dep
        except Exception:
            raise exceptions.ChartDependencyException(
                details='Could not build dependencies for {} named "{}"'.
                format(schema.TYPE_CHART,
                       chart.get('metadata').get('name')))
        else:
            return chart

    def build_chart_group(self, chart_group):
        """Builds the chart dependencies for`charts`chart group``.

        :param dict chart_group: The chart_group whose dependencies
            will be built.
        :returns: The chart_group with all dependencies.
        :rtype: dict
        :raises ManifestException: If a chart for a dependency name listed
            under ``chart_group['data']['chart_group']`` could not be found.
        """
        try:
            chart = None
            for iter, chart in enumerate(chart_group.get(
                    const.KEYWORD_DATA).get(const.KEYWORD_CHARTS, [])):
                if isinstance(chart, dict):
                    continue
                chart_object = self.find_chart_document(chart)
                self.build_chart_deps(chart_object)
                chart_group[const.KEYWORD_DATA][const.KEYWORD_CHARTS][iter] = \
                    chart_object
        except exceptions.ManifestException:
            cg_name = chart_group.get('metadata', {}).get('name')
            raise exceptions.BuildChartGroupException(
                details='Could not build {} named "{}"'.format(
                    schema.TYPE_CHARTGROUP, cg_name))

        return chart_group

    def build_armada_manifest(self):
        """Builds the Armada manifest while pulling out data
        from the chart_group.

        :returns: The Armada manifest with the data of the chart groups.
        :rtype: dict
        :raises ManifestException: If a chart group's data listed
            under ``chart_group[const.KEYWORD_DATA]`` could not be found.
        """
        for iter, group in enumerate(self.manifest.get(
                const.KEYWORD_DATA, {}).get(const.KEYWORD_GROUPS, [])):
            if isinstance(group, dict):
                continue
            chart_grp = self.find_chart_group_document(group)
            self.build_chart_group(chart_grp)

            self.manifest[const.KEYWORD_DATA][const.KEYWORD_GROUPS][iter] = \
                chart_grp

        return self.manifest

    def get_manifest(self):
        """Builds the Armada manifest

        :returns: The Armada manifest.
        :rtype: dict
        """
        self.build_armada_manifest()

        return self.manifest
