"""Define and instantiate the configuration class for Robottelo."""
import logging
import os
import sys

from logging import config
from nailgun import entities, entity_mixins
from nailgun.config import ServerConfig
from robottelo.config import casts
from six.moves.urllib.parse import urlunsplit, urljoin
from six.moves.configparser import (
    NoOptionError,
    NoSectionError,
    ConfigParser
)

LOGGER = logging.getLogger(__name__)
SETTINGS_FILE_NAME = 'robottelo.properties'


class ImproperlyConfigured(Exception):
    """Indicates that Robottelo somehow is improperly configured.

    For example, if settings file can not be found or some required
    configuration is not defined.
    """


def get_project_root():
    """Return the path to the Robottelo project root directory.

    :return: A directory path.
    :rtype: str
    """
    return os.path.realpath(os.path.join(
        os.path.dirname(__file__),
        os.pardir,
        os.pardir,
    ))


class INIReader(object):
    """ConfigParser wrapper able to cast value when reading INI options."""
    # Helper casters
    cast_boolean = casts.Boolean()
    cast_dict = casts.Dict()
    cast_list = casts.List()
    cast_logging_level = casts.LoggingLevel()
    cast_tuple = casts.Tuple()
    cast_webdriver_desired_capabilities = casts.WebdriverDesiredCapabilities()

    def __init__(self, path):
        self.config_parser = ConfigParser()
        with open(path) as handler:
            self.config_parser.readfp(handler)
            if sys.version_info[0] < 3:
                # ConfigParser.readfp is deprecated on Python3, read_file
                # replaces it
                self.config_parser.readfp(handler)
            else:
                self.config_parser.read_file(handler)

    def get(self, section, option, default=None, cast=None):
        """Read an option from a section of a INI file.

        The default value will return if the look up option is not available.
        The value will be cast using a callable if specified otherwise a string
        will be returned.

        :param section: Section to look for.
        :param option: Option to look for.
        :param default: The value that should be used if the option is not
            defined.
        :param cast: If provided the value will be cast using the cast
            provided.

        """
        try:
            value = self.config_parser.get(section, option)
            if cast is not None:
                if cast is bool:
                    value = self.cast_boolean(value)
                elif cast is dict:
                    value = self.cast_dict(value)
                elif cast is list:
                    value = self.cast_list(value)
                elif cast is tuple:
                    value = self.cast_tuple(value)
                else:
                    value = cast(value)
        except (NoSectionError, NoOptionError):
            value = default
        return value

    def has_section(self, section):
        """Check if section is available."""
        return self.config_
parser.has_section(section)


class FeatureSettings(object):
    """Settings related to a feature.

    Create a instance of this class and assign attributes to map to the feature
    options.
    """
    def read(self, reader):
        """Subclasses must imple
ment this method in order to populate itself
        with expected settings values.

        :param reader: An INIReader instance to read the settings.
        """
        raise NotImplementedError('Subclasses must implement read method.')

    def validate(self):
        """Subclasses must implement this method in order to validade the
        settings and raise ``ImproperlyConfigured`` if any issue is found.
        """
        raise NotImplementedError('Subclasses must implement validate method.')


class ServerSettings(FeatureSettings):
    """Satellite server settings definitions."""
    def __init__(self, *args, **kwargs):
        super(ServerSettings, self).__init__(*args, **kwargs)
        self.admin_password = None
        self.admin_username = None
        self.hostname = None
        self.port = None
        self.scheme = None
        self.ssh_key = None
        self.ssh_password = None
        self.ssh_username = None

    def read(self, reader):
        """Read and validate Satellite server settings."""
        self.admin_password = reader.get(
            'server', 'admin_password', 'changeme')
        self.admin_username = reader.get(
            'server', 'admin_username', 'admin')
        self.hostname = reader.get('server', 'hostname')
        self.port = reader.get('server', 'port', cast=int)
        self.scheme = reader.get('server', 'scheme', 'https')
        self.ssh_key = reader.get('server', 'ssh_key')
        self.ssh_password = reader.get('server', 'ssh_password')
        self.ssh_username = reader.get('server', 'ssh_username', 'root')

    def validate(self):
        validation_errors = []
        if self.hostname is None:
            validation_errors.append('[server] hostname must be provided.')
        if (self.ssh_key is None and self.ssh_password is None):
            validation_errors.append(
                '[server] ssh_key or ssh_password must be provided.')
        return validation_errors

    def get_credentials(self):
        """Return credentials for interacting with a Foreman deployment API.

        :return: A username-password pair.
        :rtype: tuple

        """
        return (self.admin_username, self.admin_password)

    def get_url(self):
        """Return the base URL of the Foreman deployment being tested.

        The following values from the config file are used to build the URL:

        * ``[server] scheme`` (default: https)
        * ``[server] hostname`` (required)
        * ``[server] port`` (default: none)

        Setting ``port`` to 80 does *not* imply that ``scheme`` is 'https'. If
        ``port`` is 80 and ``scheme`` is unset, ``scheme`` will still default
        to 'https'.

        :return: A URL.
        :rtype: str

        """
        if not self.scheme:
            scheme = 'https'
        else:
            scheme = self.scheme
        # All anticipated error cases have been handled at this point.
        if not self.port:
            return urlunsplit((scheme, self.hostname, '', '', ''))
        else:
            return urlunsplit((
                scheme, '{0}:{1}'.format(self.hostname, self.port), '', '', ''
            ))

    def get_pub_url(self):
        """Return the pub URL of the server being tested.

        The following values from the config file are used to build the URL:

        * ``main.server.hostname`` (required)

        :return: The pub directory URL.
        :rtype: str

        """
        return urlunsplit(('http', self.hostname, 'pub/', '', ''))

    def get_cert_rpm_url(self):
        """Return the Katello cert RPM URL of the server being tested.

        The following values from the config file are used to build the URL:

        * ``main.server.hostname`` (required)

        :return: The Katello cert RPM URL.
        :rtype: str

        """
        return urljoin(
            self.get_pub_url(), 'katello-ca-consumer-latest.noarch.rpm')


class ClientsSettings(FeatureSettings):
    """Clients settings definitions."""
    def __init__(self, *args, **kwargs):
        super(ClientsSettings, self).__init__(*args, **kwargs)
        self.image_dir = None
        self.provisioning_server = None

    def read(self, reader):
        """Read clients settings."""
        self.image_dir = reader.get(
            'clients', 'image_dir', '/opt/robottelo/images')
        self.provisioning_server = reader.get(
            'clients', 'provisioning_server')

    def validate(self):
        """Validate clients settings."""
        validation_errors = []
        if self.provisioning_server is None:
            validation_errors.append(
                '[clients] provisioning_server option must be provided.')
        return validation_errors


class DockerSettings(FeatureSettings):
    """Docker settings definitions."""
    def __init__(self, *args, **kwargs):
       