#
# Copyright (C) 2018 Red Hat, Inc.
#
# This copyrighted material is made available to anyone wishing to use,
# modify, copy, or redistribute it subject to the terms and conditions of
# the GNU General Public License v.2, or (at your option) any later version.
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY expressed or implied, including the implied warranties of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
# Public License for more details.  You should have received a copy of the
# GNU General Public License along with this program; if not, write to the
# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.  Any Red Hat trademarks that are incorporated in the
# source code or documentation are not subject to the GNU General Public
# License and may only be used or replicated with the express permission of
# Red Hat, Inc.
#
#  Author(s):  Vendula Poncova <vponcova@redhat.com>
#
import os

from pyanaconda.anaconda_loggers import get_module_logger
from pyanaconda.core.configuration.base import create_parser, read_config, get_option, \
    ConfigurationError

log = get_module_logger(__name__)


__all__ = ["ProfileLoader"]


class ProfileData(object):
    """A class that represents a profile."""

    def __init__(self):
        """Create the profile data."""
        self.config_path = ""
        self.profile_id = ""
        self.base_profile = ""
        self.os_id = ""
        self.variant_id = ""

    def load_from_file(self, config_path):
        """Load information about a profile from the given configuration file.

        :param config_path: a path to a configuration file
        :raises: ConfigurationError if a profile cannot be loaded
        """
        # Set up the parser.
        parser = create_parser()
        self._create_profile_section(parser)
        self._create_profile_detection_section(parser)

        # Read the profile sections.
        self.config_path = config_path
        read_config(parser, config_path)

        self._read_profile_section(parser)
        self._read_profile_detection_section(parser)

        if not self.profile_id:
            raise ConfigurationError("The profile id is not specified!")

    def _create_profile_section(self, parser):
        """Create the [Profile] section.

        :param parser: a configuration parser
        """
        section_name = "Profile"
        parser.add_section(section_name)
        parser.set(section_name, "profile_id", "")
        parser.set(section_name, "base_profile", "")

    def _read_profile_section(self, parser):
        """Read the [Profile] section.

        :param parser: a configuration parser
        """
        section_name = "Profile"
        self.profile_id = get_option(parser, section_name, "profile_id")
        self.base_profile = get_option(parser, section_name, "base_profile")

    def _create_profile_detection_section(self, parser):
        """Create the [Profile Detection] section.

        :param parser: a configuration parser
        """
        section_name = "Profile Detection"
        parser.add_section(section_name)
        parser.set(section_name, "os_id", "")
        parser.set(section_name, "variant_id", "")

    def _read_profile_detection_section(self, parser):
        """Read the [Profile Detection] section.

        :param parser: a configuration parser
        """
        section_name = "Profile Detection"
        self.os_id = get_option(parser, section_name, "os_id")
        self.variant_id = get_option(parser, section_name, "variant_id")


class ProfileLoader(object):
    """A class for loading information about profiles from configuration files."""

    def __init__(self):
        """Create a new loader."""
        self._profiles = {}

    def load_profiles(self, config_dir):
        """Load information about profiles from the given configuration directory.

        Invalid configuration files will be skipped.

        :param config_dir: a path to a directory
        """
        log.info("Loading information about profiles from %s.", config_dir)

        for file_name in sorted(os.listdir(config_dir)):
            if not file_name.endswith(".conf"):
                continue

            config_path = os.path.join(config_dir, file_name)

            try:
                self.load_profile(config_path)
            except ConfigurationError as e:
                log.error("Skipping an invalid configuration at %s: %s", config_path, e)

    def load_profile(self, config_path):
        """Load information about a profile from the given configuration file.

        :param config_path: a path to a configuration file
        :raises: ConfigurationError if a profile cannot be loaded
        """
        data = ProfileData()

        # Load the profile.
        data.load_from_file(config_path)
        profile_id = data.profile_id

        if profile_id in self._profiles:
            raise ConfigurationError("The '{}' profile was already loaded.".format(profile_id))

        # Add the profile.
        log.info("Found the '%s' profile at %s.", profile_id, config_path)
        self._profiles[profile_id] = data

    def check_profile(self, profile_id):
        """Check if the specified profile is supported.

        :param profile_id: an id of the profile
        :return: True if the profile is supported, otherwise False
        """
        if profile_id not in self._profiles:
            log.warning("No support for the '%s' profile.", profile_id)
            return False

        try:
            self._get_profile_bases(profile_id)
        except ConfigurationError as e:
            log.warning("Invalid support for the '%s' profile: %s", profile_id, e)
            return False

        return True

    def detect_profile(self, os_id, variant_id=None):
        """Find a profile that matches the specified values.

        :param str os_id: an id of the operating system or None
        :param str variant_id: an id of a specific variant of the operating system or None
        :return: a product id or None
        """
        log.debug("Detecting a profile for ID=%s, VARIANT_ID=%s.", os_id, variant_id)

        # Collect keys and profiles for the detection.
        profiles = {}

        for profile_id, data in self._profiles.items():
            key = (data.os_id, data.variant_id)

            # The profile is not detectable.
            if not any(key):
                continue

            profiles[key] = profile_id

        # Find a profile with the best match.
        profile_id = profiles.get((os_id, variant_id)) or profiles.get((os_id, ""))

        if profile_id:
            log.info("The '%s' profile is detected.", profile_id)

        return profile_id

    def collect_configurations(self, profile_id):
        """Collect configuration files of the given profile.

        The configuration files should be processed in the given order.

        :param profile_id: an id of the profile
        :return: a list of paths to configuration files
        """
        return self._get_profile_configs(profile_id)

    def _get_profile_config(self, profile_id):
        """Get the configuration path of the profile.

        :param profile_id: an id of the profile
        :return: a path to a configuration file
        """
        return self._profiles.get(profile_id).config_path

    def _get_profile_configs(self, profile_id):
        """Get a list of configuration paths of the profile.

        The configuration files should be processed in the given order.

        :param profile_id: an id of the profile
        :return: a list of paths to a configuration files
        """
        profile_ids = reversed(self._get_profile_bases(profile_id))
        return [self._get_profile_config(i) for i in profile_ids]

    def _get_profile_base(self, profile_id):
        """Get the base of the profile.

        :param profile_id: an id of the profile
        :return: an id of the base profile
        """
        return self._profiles.get(profile_id).base_profile

    def _get_profile_bases(self, profile_id):
        """Return a list of bases of the given profile.

        The profiles are ordered by the "is based on" relation.

        If the profile A is based on the profile B and the profile B
        is based on the profile C, then the related profiles of the
        profile A are: A, B, C

        :param profile_id: an id of the profile
        :return: a list of ids of the base profiles
        :raises: ConfigurationError if the dependencies cannot be resolved
        """
        current_id = profile_id
        visited = set()
        profiles = []

        while current_id:
            if current_id not in self._profiles:
                raise ConfigurationError(
                    "Dependencies of the '{}' profile cannot be resolved "
                    "due to an unknown '{}' profile.".format(profile_id, current_id)
                )

            if current_id in visited:
                raise ConfigurationError(
                    "Dependencies of the '{}' profile cannot be resolved "
                    "due to a conflict with '{}'.".format(profile_id, current_id)
                )

            visited.add(current_id)
            profiles.append(current_id)
            current_id = self._get_profile_base(current_id)

        return profiles
