import os
import yaml
import logbook
import jsonschema

from piper.utils import dynamic_load


DB_SCHEMA = {
    'description': 'Database configuration',
    'type': 'object',
    'required': ['host'],
    'properties': {
        'host': {
            'description': 'The host to connect to',
            'type': 'string',
        },
        'user': {
            'description': 'The username used for authentication',
            'type': ['string', 'null'],
        },
        'password': {
            'description': 'The passord used for authentication',
            'type': ['string', 'null'],
        },
    },
}


class ConfigError(Exception):
    pass


class ConfigBase(object):
    def __init__(self, filename):
        self.filename = filename

        self.raw = None
        self.classes = {}

        self.log = logbook.Logger(self.__class__.__name__)

    def load(self):
        self.log.debug('Loading configuration')
        self.load_config()
        self.validate_config()
        self.load_classes()
        return self

    def load_config(self):
        """
        Parses the configuration file and dies in flames if there are errors.

        """

        if not os.path.isfile(self.filename):
            err = 'Config file not found in $PWD. Aborting.'
            self.log.error(err)
            raise ConfigError(err)

        with open(self.filename) as config:
            file_data = config.read()

            try:
                self.raw = yaml.safe_load(file_data)

            except yaml.parser.ParserError as exc:
                self.log.error(exc)
                err = 'Invalid YAML in {0}. Aborting.'.format(self.filename)
                self.log.error(err)
                raise ConfigError(err)

        self.log.debug('Configuration file loaded.')

    def validate_config(self):
        self.log.debug('Validating root config...')
        jsonschema.validate(self.raw, self.schema)

    def load_classes(self):  # pragma: nocover
        self.log.debug("No class loading definitions set")


class BuildConfig(ConfigBase):
    schema = {
        "$schema": "http://json-schema.org/draft-04/schema",
        'type': 'object',
        'additionalProperties': False,
        'required': ['version', 'envs', 'steps', 'jobs'],
        'properties': {
            'version': {
                'description':
                    'Versioning setup for this project. This sets up what '
                    'commands to run to determine the version of the job '
                    'being executed',
                'type': 'object',
            },
            'envs': {
                'description': 'The env configuration for this build.',
                'type': 'object',
                'additionalProperties': {
                    'type': 'object',
                },
            },
            'steps': {
                'description': 'Definitions of executable build steps.',
                'type': 'object',
            },
            'jobs': {
                'description': 'Runnable collections of steps.',
                'type': 'object',
                'additionalProperties': {
                    'type': 'array',
                    'items': {'type': 'string'},
                },
            },
            'db': DB_SCHEMA,
        },
    }

    def __init__(self, filename=None):
        if not filename:
            filename = 'piper.yml'

        super(BuildConfig, self).__init__(filename)

    def load_classes(self):
        self.log.debug("Loading classes...")

        targets = set()

        targets.add(self.raw['version']['class'])
        targets.add(self.raw['db']['class'])

        for env in self.raw['envs'].values():
            targets.add(env['class'])

        for step in self.raw['steps'].values():
            targets.add(step['class'])

        for cls in targets:
            self.log.debug("Loading class '{0}()'".format(cls))
            self.classes[cls] = dynamic_load(cls)

        self.log.debug("Class loading done.")

    def merge_namespace(self, ns):
        """
        Take an argparse namespace and merge whatever it had directly in to the
        configuration object.

        Before this, we used to shuffle around both, to mostly the same use.

        """

        self.log.debug('Merging argparse namespace')
        for key in filter(lambda x: not x.startswith('_'), dir(ns)):
            attr = getattr(ns, key)
            setattr(self, key, attr)

    def get_database(self):
        return self.classes[self.raw['db']['class']]()

@@ -1800,39 +1800,254 @@
 def 
-validate_config(self):%0A
+collect_classes(self):  # pragma: nocover%0A        self.log.debug(%22No class collection defined%22)%0A        return set()%0A%0A    def load_classes(self):%0A        self.log.debug(%22Loading classes...%22)%0A%0A        for cls in self.collect_classes():%0A   
         
 self
@@ -2034,32 +2034,33 @@
 s():%0A           
+ 
 self.log.debug('
@@ -2062,30 +2062,207 @@
 bug(
-'Validating root confi
+%22Loading class '%7B0%7D()'%22.format(cls))%0A            self.classes%5Bcls%5D = dynamic_load(cls)%0A%0A        self.log.debug(%22Class loading done.%22)%0A%0A    def validate_config(self):%0A        self.log.debug('Validatin
 g...
@@ -2328,104 +2328,526 @@
 def 
-load_classes
+merge_namespace
 (self
+, ns
 ):
+%0A
   
-# pragma: nocover%0A        self.log.debug(%22No class loading definitions set%22
+      %22%22%22%0A        Take an argparse namespace and merge whatever it had directly in to the%0A        configuration object.%0A%0A        Before this, we used to shuffle around both, to mostly the same use.%0A%0A        %22%22%22%0A%0A        self.log.debug('Merging argparse namespace')%0A        for key in filter(lambda x: not x.startswith('_'), dir(ns)):%0A            attr = getattr(ns, key)%0A            setattr(self, key, attr)%0A%0A    def get_database(self):%0A        return self.classes%5Bself.raw%5B'db'%5D%5B'class'%5D%5D(
 )%0A%0A%0A
@@ -4264,36 +4264,39 @@
 ename)%0A%0A    def 
-load
+collect
 _classes(self):%0A
@@ -4299,54 +4299,8 @@
 f):%0A
-        self.log.debug(%22Loading classes...%22)%0A%0A
     
@@ -4601,714 +4601,19 @@
     
-for cls in targets:%0A            self.log.debug(%22Loading class '%7B0%7D()'%22.format(cls))%0A            self.classes%5Bcls%5D = dynamic_load(cls)%0A%0A        self.log.debug(%22Class loading done.%22)%0A%0A    def merge_namespace(self, ns):%0A        %22%22%22%0A        Take an argparse namespace and merge whatever it had directly in to the%0A        configuration object.%0A%0A        Before this, we used to shuffle around both, to mostly the same use.%0A%0A        %22%22%22%0A%0A        self.log.debug('Merging argparse namespace')%0A        for key in filter(lambda x: not x.startswith('_'), dir(ns)):%0A            attr = getattr(ns, key)%0A            setattr(self, key, attr)%0A%0A    def get_database(self):%0A        return self.classes%5Bself.raw%5B'db'%5D%5B'class'%5D%5D()
+return targets
 %0A

Refactor ConfigBase() and BuildConfig()