from enum import Enum
from configparser import ConfigParser

from utils.errors import ConfigError, ValidationError


class SpecialSchemaProcessing(Enum):
    """These enums define how processing should be applied to a string to
separate it out into list.
    """
    commma_separated = 1
    space_separated = 2


class ConfigSchemaLine():
    """Defines a config line by what the key is, if it is required or optional,
the type that the value should be processed as, and any special processing required
"""

    def __init__(self, key: str, required: bool, expected_type, special_processing: SpecialSchemaProcessing=None):
        self.key = key
        self.required = required
        self.expected_type = expected_type
        if expected_type is list:
            if type(special_processing) is not SpecialSchemaProcessing:
                raise ValidationError("ValidationError: special_processing must be defined if implementing a list in config")
        self.special_processing = special_processing

    def __str__(self):
        return (f'<{self.__class__.__name__} - '
                f'Key:{self.key}, '
                f'Required:{self.required}, '
                f'Type:{self.expected_type}, '
                f'Processing: {self.special_processing}'
                f'>')


class ConfigSchemaSection():
    """Defines a schema to be used. Consists of a single section name and
multiple config lines"""

    def __init__(self, section_name, options: [ConfigSchemaLine]):
        self.section_name = section_name
        self.options = {}
        self._process_options(options)

    def _process_options(self, options):
        if options is None:
            return
        if type(options) is not list:
            raise ValidationError(f"ValidationError: {self.__class__.__name__} options must be '[ConfigSchemaLine]' or 'None'")
        if len(options) == 0:
            return
        for o in options:
            self.add_line(o)

    def add_line(self, line: ConfigSchemaLine):
        self.options[line.key] = line

    def key_in_options(self, key: str) -> bool:
        return key in self.options.keys()

    def get_schema_line(self, key: str) -> ConfigSchemaLine:
        if not self.key_in_options(key):
            raise KeyError(f"KeyError: '{key}' is not present in schema section")
        return self.options[key]

    def __str__(self):
        options_strs = [ o.__str__() for o in self.options]
        return (f'{self.__class__.__name__} named "{self.section_name}" with {options_strs}')


class ConfigSchema():

    def __init__(self, sections: [ConfigSchemaSection]):
        self.sections = {}
        self._process_sections(sections)

    def _process_sections(self, sections):
        if sections is None:
            return
        if type(sections) is not list:
            raise ValidationError(f"ValidationError: {self.__class__.__name__} sections must be '[ConfigSchemaSection]' or 'None'")
        if len(sections) == 0:
            return
        for sec in sections:
            self.add_section(sec)

    def add_section(self, section: ConfigSchemaSection):
        self.sections[section.section_name] = section

    def section_exists(self, section_name: str) -> bool:
        return section_name in self.sections.keys()

    def get_schema_section(self, section_name: str) -> ConfigSchemaSection:
        if not self.section_exists(section_name):
            raise KeyError(f"KeyError: '{section_name}' is not present in schema")
        return self.sections[section_name]

    def __str__(self):
        section_strs = [ o.__str__() for o in self.sections]
        return (f'{self.__class__.__name__} with {section_strs}')


def _split_string_to_list(items: str, separator=',') -> [str]:
    split_items = items.split(separator)
    processed_items = [ i.strip(' ').strip('\n') for i in split_items]
    return processed_items


def _parse_config_section(config: ConfigParser(), schema: ConfigSchemaSection) -> dict:
    """For a given config it will extract all values defined by the schema
and return them as a dictionary of the keys and the assoicated value as the
type defined by the schema
"""
    parsed_items = {}

    section = schema.section_name
    for csl_key in schema.options:
        csl = schema.get_schema_line(csl_key)

        # Parses each line
        if config.has_option(section, csl_key):
            if csl.expected_type is bool:
                parsed_items[csl.key] = config.getboolean(section, csl.key)
            elif csl.expected_type is int:
                parsed_items[csl.key] = config.getint(section, csl.key)
            elif csl.expected_type is float:
                parsed_items[csl.key] = config.getfloat(section, csl.key)
            elif csl.expected_type is str:
                parsed_items[csl.key] = config[section][csl.key]
            elif csl.expected_type is list:
                if csl.special_processing == SpecialSchemaProcessing.commma_separated:
                    items = config[section][csl.key]
                    splitup = _split_string_to_list(items, ',')
                    parsed_items[csl.key] = splitup
                elif csl.special_processing == SpecialSchemaProcessing.space_separated:
                    items = config[section][csl.key]
                    splitup = _split_string_to_list(items, ' ')
                    parsed_items[csl.key] = splitup
                else:

                    raise ConfigError(config, f"ConfigError: Don't know how to process special schema {csl.special_processing}")
            else:
                raise ConfigError(config, f"ConfigError: Type '{csl.expected_type}' cannot be processed - need to modify {__name__}")

        else:
            if csl.required:
                raise ConfigError(config, f"ConfigError: Key '{csl.key}' is required but was not found in config")

    return parsed_items


def _parse_config(config: ConfigParser(), schema: ConfigSchema) -> dict:
    parsed_items = {}

    for css_name in schema.sections:
        css = schema.get_schema_section(css_name)

        # if config.has_section(css_name):
        parsed_section = _parse_config_section(config, css)
        parsed_items[css_name] = parsed_section
        # else:
        #     raise ConfigError(config, f"ConfigError: Expected section '{css_name}' from schema not present in config")

    return parsed_items


class ParsedConfig():

    def __init__(self, config: ConfigParser, schema: ConfigSchema):
        self._section_dict = _parse_config(config, schema)

    def has_section(self, section_name):
        return section_name in self._section_dict

    def get_section(self, section_name):
        if self.has_section(section_name):
            return self._section_dict[section_name]
        return None
