import os

FIELD_TO_PARSE = object()


class ConfigurationError(Exception):
    def __init__(self, *args, location):
        self.location = location
        super().__init__(*args)

    def __str__(self):
        return f'Error parsing config:\n{self.location}\n  {super().__str__()}'


def parse_config(obj):
    if isinstance(obj, LazilyParsedConfig):
        obj.parse_fields()
    elif isinstance(obj, list):
        for o in obj:
            parse_config(o)
    elif isinstance(obj, dict):
        for o in obj.values():
            parse_config(o)


class LazilyParsedConfig:
    def __init__(self, config: dict, steps: tuple = ()):
        self.raw_data = config
        self.steps = steps

    def parse_fields(self):
        for attribute in self.__dict__:
            _, prefix, name = attribute.partition('_field_')
            if prefix:
                parse_config(getattr(self, name))

    def raise_error(self, message, *, extra_steps=()):
        import inspect

        field = inspect.currentframe().f_back.f_code.co_name
        raise ConfigurationError(message, location=' -> '.join([*self.steps, field, *extra_steps]))


class RootConfig(LazilyParsedConfig):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self._field_mode = FIELD_TO_PARSE
        self._field_project = FIELD_TO_PARSE
        self._field_shell = FIELD_TO_PARSE
        self._field_dirs = FIELD_TO_PARSE
        self._field_projects = FIELD_TO_PARSE
        self._field_publish = FIELD_TO_PARSE
        self._field_template = FIELD_TO_PARSE
        self._field_terminal = FIELD_TO_PARSE

    @property
    def mode(self):
        if self._field_mode is FIELD_TO_PARSE:
            if 'mode' in self.raw_data:
                mode = self.raw_data['mode']
                if not isinstance(mode, str):
                    self.raise_error('must be a string')

                valid_modes = ('aware', 'local', 'project')
                if mode not in valid_modes:
                    self.raise_error(f'must be one of: {", ".join(valid_modes)}')

                self._field_mode = mode
            else:
                self._field_mode = self.raw_data['mode'] = 'local'

        return self._field_mode

    @mode.setter
    def mode(self, value):
        self.raw_data['mode'] = value
        self._field_mode = FIELD_TO_PARSE

    @property
    def project(self):
        if self._field_project is FIELD_TO_PARSE:
            if 'project' in self.raw_data:
                project = self.raw_data['project']
                if not isinstance(project, str):
                    self.raise_error('must be a string')

                self._field_project = project
            else:
                self._field_project = self.raw_data['project'] = ''

        return self._field_project

    @project.setter
    def project(self, value):
        self.raw_data['project'] = value
        self._field_project = FIELD_TO_PARSE

    @property
    def shell(self):
        if self._field_shell is FIELD_TO_PARSE:
            if 'shell' in self.raw_data:
                shell = self.raw_data['shell']
                if isinstance(shell, str):
                    self._field_shell = ShellConfig({'name': shell}, ('shell',))
                elif isinstance(shell, dict):
                    self._field_shell = ShellConfig(shell, ('shell',))
                else:
                    self.raise_error('must be a string or table')
            else:
                self.raw_data['shell'] = ''
                self._field_shell = ShellConfig({'name': ''}, ('shell',))

        return self._field_shell

    @shell.setter
    def shell(self, value):
        self.raw_data['shell'] = value
        self._field_shell = FIELD_TO_PARSE

    @property
    def dirs(self):
        if self._field_dirs is FIELD_TO_PARSE:
            if 'dirs' in self.raw_data:
                dirs = self.raw_data['dirs']
                if not isinstance(dirs, dict):
                    self.raise_error('must be a table')

                self._field_dirs = DirsConfig(dirs, ('dirs',))
            else:
                dirs = {}
                self.raw_data['dirs'] = dirs
                self._field_dirs = DirsConfig(dirs, ('dirs',))

        return self._field_dirs

    @dirs.setter
    def dirs(self, value):
        self.raw_data['dirs'] = value
        self._field_dirs = FIELD_TO_PARSE

    @property
    def projects(self):
        if self._field_projects is FIELD_TO_PARSE:
            if 'projects' in self.raw_data:
                projects = self.raw_data['projects']
                if not isinstance(projects, dict):
                    self.raise_error('must be a table')

                project_data = {}
                for name, data in projects.items():
                    if isinstance(data, str):
                        project_data[name] = ProjectConfig({'location': data}, ('projects', name))
                    elif isinstance(data, dict):
                        project_data[name] = ProjectConfig(data, ('projects', name))
                    else:
                        self.raise_error('must be a string or table', extra_steps=(name,))

                self._field_projects = project_data
            else:
                self._field_projects = self.raw_data['projects'] = {}

        return self._field_projects

    @projects.setter
    def projects(self, value):
        self.raw_data['projects'] = value
        self._field_projects = FIELD_TO_PARSE

    @property
    def publish(self):
        if self._field_publish is FIELD_TO_PARSE:
            if 'publish' in self.raw_data:
                publish = self.raw_data['publish']
                if not isinstance(publish, dict):
                    self.raise_error('must be a table')

                for name, data in publish.items():
                    if not isinstance(data, dict):
                        self.raise_error('must be a table', extra_steps=(name,))

                self._field_publish = publish
            else:
                self._field_publish = self.raw_data['publish'] = {'index': {'repo': 'main'}}

        return self._field_publish

    @publish.setter
    def publish(self, value):
        self.raw_data['publish'] = value
        self._field_publish = FIELD_TO_PARSE

    @property
    def template(self):
        if self._field_template is FIELD_TO_PARSE:
            if 'template' in self.raw_data:
                template = self.raw_data['template']
                if not isinstance(template, dict):
                    self.raise_error('must be a table')

                self._field_template = TemplateConfig(template, ('template',))
            else:
                template = {}
                self.raw_data['template'] = template
                self._field_template = TemplateConfig(template, ('template',))

        return self._field_template

    @template.setter
    def template(self, value):
        self.raw_data['template'] = value
        self._field_template = FIELD_TO_PARSE

    @property
    def terminal(self):
        if self._field_terminal is FIELD_TO_PARSE:
            if 'terminal' in self.raw_data:
                terminal = self.raw_data['terminal']
                if not isinstance(terminal, dict):
                    self.raise_error('must be a table')

                self._field_terminal = TerminalConfig(terminal, ('terminal',))
            else:
                terminal = {}
                self.raw_data['terminal'] = terminal
                self._field_terminal = TerminalConfig(terminal, ('terminal',))

        return self._field_terminal

    @terminal.setter
    def terminal(self, value):
        self.raw_data['terminal'] = value
        self._field_terminal = FIELD_TO_PARSE


class ShellConfig(LazilyParsedConfig):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self._field_name = FIELD_TO_PARSE
        self._field_path = FIELD_TO_PARSE
        self._field_args = FIELD_TO_PARSE

    @property
    def name(self):
        if self._field_name is FIELD_TO_PARSE:
            if 'name' in self.raw_data:
                name = self.raw_data['name']
                if not isinstance(name, str):
                    self.raise_error('must be a string')

                self._field_name = name
            else:
                self.raise_error('required field')

        return self._field_name

    @name.setter
    def name(self, value):
        self.raw_data['name'] = value
        self._field_name = FIELD_TO_PARSE

    @property
    def path(self):
        if self._field_path is FIELD_TO_PARSE:
            if 'path' in self.raw_data:
                path = self.raw_data['path']
                if not isinstance(path, str):
                    self.raise_error('must be a string')

                self._field_path = path
            else:
                self._field_path = self.raw_data['path'] = self.name

        return self._field_path

    @path.setter
    def path(self, value):
        self.raw_data['path'] = value
        self._field_path = FIELD_TO_PARSE

    @property
    def args(self):
        if self._field_args is FIELD_TO_PARSE:
            if 'args' in self.raw_data:
                args = self.raw_data['args']
                if not isinstance(args, list):
                    self.raise_error('must be an array')

                for i, entry in enumerate(args, 1):
                    if not isinstance(entry, str):
                        self.raise_error('must be a string', extra_steps=(str(i),))

                self._field_args = args
            else:
                self._field_args = self.raw_data['args'] = []

        return self._field_args

    @args.setter
    def args(self, value):
        self.raw_data['args'] = value
        self._field_args = FIELD_TO_PARSE


class DirsConfig(LazilyParsedConfig):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self._field_project = FIELD_TO_PARSE
        self._field_env = FIELD_TO_PARSE
        self._field_python = FIELD_TO_PARSE
        self._field_data = FIELD_TO_PARSE
        self._field_cache = FIELD_TO_PARSE

    @property
    def project(self):
        if self._field_project is FIELD_TO_PARSE:
            if 'project' in self.raw_data:
                project = self.raw_data['project']
                if not isinstance(project, list):
                    self.raise_error('must be an array')

                for i, entry in enumerate(project, 1):
                    if not isinstance(entry, str):
                        self.raise_error('must be a string', extra_steps=(str(i),))

                self._field_project = project
            else:
                self._field_project = self.raw_data['project'] = []

        return self._field_project

    @project.setter
    def project(self, value):
        self.raw_data['project'] = value
        self._field_project = FIELD_TO_PARSE

    @property
    def env(self):
        if self._field_env is FIELD_TO_PARSE:
            if 'env' in self.raw_data:
                env = self.raw_data['env']
                if not isinstance(env, dict):
                    self.raise_error('must be a table')

                for key, value in env.items():
                    if not isinstance(value, str):
                        self.raise_error('must be a string', extra_steps=(key,))

                self._field_env = env
            else:
                self._field_env = self.raw_data['env'] = {}

        return self._field_env

    @env.setter
    def env(self, value):
        self.raw_data['env'] = value
        self._field_env = FIELD_TO_PARSE

    @property
    def python(self):
        if self._field_python is FIELD_TO_PARSE:
            if 'python' in self.raw_data:
                python = self.raw_data['python']
                if not isinstance(python, str):
                    self.raise_error('must be a string')

                self._field_python = python
            else:
                self._field_python = self.raw_data['python'] = 'isolated'

        return self._field_python

    @python.setter
    def python(self, value):
        self.raw_data['python'] = value
        self._field_python = FIELD_TO_PARSE

    @property
    def data(self):
        if self._field_data is FIELD_TO_PARSE:
            if 'data' in self.raw_data:
                data = self.raw_data['data']
                if not isinstance(data, str):
                    self.raise_error('must be a string')

                self._field_data = data
            else:
                from platformdirs import user_data_dir

                self._field_data = self.raw_data['data'] = user_data_dir('hatch', appauthor=False)

        return self._field_data

    @data.setter
    def data(self, value):
        self.raw_data['data'] = value
        self._field_data = FIELD_TO_PARSE

    @property
    def cache(self):
        if self._field_cache is FIELD_TO_PARSE:
            if 'cache' in self.raw_data:
                cache = self.raw_data['cache']
                if not isinstance(cache, str):
                    self.raise_error('must be a string')

                self._field_cache = cache
            else:
                from platformdirs import user_cache_dir

                self._field_cache = self.raw_data['cache'] = user_cache_dir('hatch', appauthor=False)

        return self._field_cache

    @cache.setter
    def cache(self, value):
        self.raw_data['cache'] = value
        self._field_cache = FIELD_TO_PARSE


class ProjectConfig(LazilyParsedConfig):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self._field_location = FIELD_TO_PARSE

    @property
    def location(self):
        if self._field_location is FIELD_TO_PARSE:
            if 'location' in self.raw_data:
                location = self.raw_data['location']
                if not isinstance(location, str):
                    self.raise_error('must be a string')

                self._field_location = location
            else:
                self.raise_error('required field')

        return self._field_location

    @location.setter
    def location(self, value):
        self.raw_data['location'] = value
        self._field_location = FIELD_TO_PARSE


class TemplateConfig(LazilyParsedConfig):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self._field_name = FIELD_TO_PARSE
        self._field_email = FIELD_TO_PARSE
        self._field_licenses = FIELD_TO_PARSE
        self._field_plugins = FIELD_TO_PARSE

    @property
    def name(self):
        if self._field_name is FIELD_TO_PARSE:
            if 'name' in self.raw_data:
                name = self.raw_data['name']
                if not isinstance(name, str):
                    self.raise_error('must be a string')

                self._field_name = name
            else:
                name = os.environ.get('GIT_AUTHOR_NAME')
                if name is None:
                    import subprocess

                    try:
                        name = subprocess.check_output(['git', 'config', '--get', 'user.name'], text=True).strip()
                    except Exception:
                        name = 'U.N. Owen'

                self._field_name = self.raw_data['name'] = name

        return self._field_name

    @name.setter
    def name(self, value):
        self.raw_data['name'] = value
        self._field_name = FIELD_TO_PARSE

    @property
    def email(self):
        if self._field_email is FIELD_TO_PARSE:
            if 'email' in self.raw_data:
                email = self.raw_data['email']
                if not isinstance(email, str):
                    self.raise_error('must be a string')

                self._field_email = email
            else:
                email = os.environ.get('GIT_AUTHOR_EMAIL')
                if email is None:
                    import subprocess

                    try:
                        email = subprocess.check_output(['git', 'config', '--get', 'user.email'], text=True).strip()
                    except Exception:
                        email = 'void@some.where'

                self._field_email = self.raw_data['email'] = email

        return self._field_email

    @email.setter
    def email(self, value):
        self.raw_data['email'] = value
        self._field_email = FIELD_TO_PARSE

    @property
    def licenses(self):
        if self._field_licenses is FIELD_TO_PARSE:
            if 'licenses' in self.raw_data:
                licenses = self.raw_data['licenses']
                if not isinstance(licenses, dict):
                    self.raise_error('must be a table')

                self._field_licenses = LicensesConfig(licenses, self.steps + ('licenses',))
            else:
                licenses = {}
                self.raw_data['licenses'] = licenses
                self._field_licenses = LicensesConfig(licenses, self.steps + ('licenses',))

        return self._field_licenses

    @licenses.setter
    def licenses(self, value):
        self.raw_data['licenses'] = value
        self._field_licenses = FIELD_TO_PARSE

    @property
    def plugins(self):
        if self._field_plugins is FIELD_TO_PARSE:
            if 'plugins' in self.raw_data:
                plugins = self.raw_data['plugins']
                if not isinstance(plugins, dict):
                    self.raise_error('must be a table')

                for name, data in plugins.items():
                    if not isinstance(data, dict):
                        self.raise_error('must be a table', extra_steps=(name,))

                self._field_plugins = plugins
            else:
                self._field_plugins = self.raw_data['plugins'] = {
                    'default': {'tests': True, 'ci': False, 'src-layout': False}
                }

        return self._field_plugins

    @plugins.setter
    def plugins(self, value):
        self.raw_data['plugins'] = value
        self._field_plugins = FIELD_TO_PARSE


class LicensesConfig(LazilyParsedConfig):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self._field_headers = FIELD_TO_PARSE
        self._field_default = FIELD_TO_PARSE

    @property
    def headers(self):
        if self._field_headers is FIELD_TO_PARSE:
            if 'headers' in self.raw_data:
                headers = self.raw_data['headers']
                if not isinstance(headers, bool):
                    self.raise_error('must be a boolean')

                self._field_headers = headers
            else:
                self._field_headers = self.raw_data['headers'] = True

        return self._field_headers

    @headers.setter
    def headers(self, value):
        self.raw_data['headers'] = value
        self._field_headers = FIELD_TO_PARSE

    @property
    def default(self):
        if self._field_default is FIELD_TO_PARSE:
            if 'default' in self.raw_data:
                default = self.raw_data['default']
                if not isinstance(default, list):
                    self.raise_error('must be an array')

                for i, entry in enumerate(default, 1):
                    if not isinstance(entry, str):
                        self.raise_error('must be a string', extra_steps=(str(i),))

                self._field_default = default
            else:
                self._field_default = self.raw_data['default'] = ['MIT']

        return self._field_default

    @default.setter
    def default(self, value):
        self.raw_data['default'] = value
        self._field_default = FIELD_TO_PARSE


class TerminalConfig(LazilyParsedConfig):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self._field_styles = FIELD_TO_PARSE

    @property
    def styles(self):
        if self._field_styles is FIELD_TO_PARSE:
            if 'styles' in self.raw_data:
                styles = self.raw_data['styles']
                if not isinstance(styles, dict):
                    self.raise_error('must be a table')

                self._field_styles = StylesConfig(styles, self.steps + ('styles',))
            else:
                styles = {}
                self.raw_data['styles'] = styles
                self._field_styles = StylesConfig(styles, self.steps + ('styles',))

        return self._field_styles

    @styles.setter
    def styles(self, value):
        self.raw_data['styles'] = value
        self._field_styles = FIELD_TO_PARSE


class StylesConfig(LazilyParsedConfig):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self._field_info = FIELD_TO_PARSE
        self._field_success = FIELD_TO_PARSE
        self._field_error = FIELD_TO_PARSE
        self._field_warning = FIELD_TO_PARSE
        self._field_waiting = FIELD_TO_PARSE
        self._field_debug = FIELD_TO_PARSE
        self._field_spinner = FIELD_TO_PARSE

    @property
    def info(self):
        if self._field_info is FIELD_TO_PARSE:
            if 'info' in self.raw_data:
                info = self.raw_data['info']
                if not isinstance(info, str):
                    self.raise_error('must be a string')

                self._field_info = info
            else:
                self._field_info = self.raw_data['info'] = 'bold'

        return self._field_info

    @info.setter
    def info(self, value):
        self.raw_data['info'] = value
        self._field_info = FIELD_TO_PARSE

    @property
    def success(self):
        if self._field_success is FIELD_TO_PARSE:
            if 'success' in self.raw_data:
                success = self.raw_data['success']
                if not isinstance(success, str):
                    self.raise_error('must be a string')

                self._field_success = success
            else:
                self._field_success = self.raw_data['success'] = 'bold cyan'

        return self._field_success

    @success.setter
    def success(self, value):
        self.raw_data['success'] = value
        self._field_success = FIELD_TO_PARSE

    @property
    def error(self):
        if self._field_error is FIELD_TO_PARSE:
            if 'error' in self.raw_data:
                error = self.raw_data['error']
                if not isinstance(error, str):
                    self.raise_error('must be a string')

                self._field_error = error
            else:
                self._field_error = self.raw_data['error'] = 'bold red'

        return self._field_error

    @error.setter
    def error(self, value):
        self.raw_data['error'] = value
        self._field_error = FIELD_TO_PARSE

    @property
    def warning(self):
        if self._field_warning is FIELD_TO_PARSE:
            if 'warning' in self.raw_data:
                warning = self.raw_data['warning']
                if not isinstance(warning, str):
                    self.raise_error('must be a string')

                self._field_warning = warning
            else:
                self._field_warning = self.raw_data['warning'] = 'bold yellow'

        return self._field_warning

    @warning.setter
    def warning(self, value):
        self.raw_data['warning'] = value
        self._field_warning = FIELD_TO_PARSE

    @property
    def waiting(self):
        if self._field_waiting is FIELD_TO_PARSE:
            if 'waiting' in self.raw_data:
                waiting = self.raw_data['waiting']
                if not isinstance(waiting, str):
                    self.raise_error('must be a string')

                self._field_waiting = waiting
            else:
                self._field_waiting = self.raw_data['waiting'] = 'bold magenta'

        return self._field_waiting

    @waiting.setter
    def waiting(self, value):
        self.raw_data['waiting'] = value
        self._field_waiting = FIELD_TO_PARSE

    @property
    def debug(self):
        if self._field_debug is FIELD_TO_PARSE:
            if 'debug' in self.raw_data:
                debug = self.raw_data['debug']
                if not isinstance(debug, str):
                    self.raise_error('must be a string')

                self._field_debug = debug
            else:
                self._field_debug = self.raw_data['debug'] = 'bold'

        return self._field_debug

    @debug.setter
    def debug(self, value):
        self.raw_data['debug'] = value
        self._field_debug = FIELD_TO_PARSE

    @property
    def spinner(self):
        if self._field_spinner is FIELD_TO_PARSE:
            if 'spinner' in self.raw_data:
                spinner = self.raw_data['spinner']
                if not isinstance(spinner, str):
                    self.raise_error('must be a string')

                self._field_spinner = spinner
            else:
                self._field_spinner = self.raw_data['spinner'] = 'simpleDotsScrolling'

        return self._field_spinner

    @spinner.setter
    def spinner(self, value):
        self.raw_data['spinner'] = value
        self._field_spinner = FIELD_TO_PARSE
