# Copyright (C) 2018, 2019, 2020, 2021 The Meme Factory, Inc.
# http://www.karlpinc.com/

# This file is part of PGWUI_Server.
#
# This program is free software: you can redistribute it and/or
# modify it under the terms of the GNU Affero General Public License
# as published by the Free Software Foundation, either version 3 of
# the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public
# License along with this program.  If not, see
# <http://www.gnu.org/licenses/>.
#

# Karl O. Pinc <kop@karlpinc.com>

'''Load the PGWUI components, parse the PGWUI configuration, and start the
WSGI server.
'''

from pyramid.config import Configurator
import pyramid.exceptions
import logging
import sys

from . import exceptions as server_ex
from pgwui_common import assets
from pgwui_common import check_settings
from pgwui_common import exceptions as common_ex
from pgwui_common import routes
from pgwui_common import plugin
import pgwui_common.urls

# Constants

# All the single-valued settings recognized by PGWUI_Server/Core
SETTINGS = set(
    ['pg_host',
     'pg_port',
     'default_db',
     'dry_run',
     'route_prefix',
     'validate_hmac',
     'autoconfigure',
     ])

# All the multi-valued settings recognized by PGWUI_Server/Core
MULTI_SETTINGS = set(
    ['routes',
     'home_page',
     'menu_page',
     'override_assets',
     ])

# Default settings
DEFAULT_HOME_PAGE_TYPE = 'URL'
DEFAULT_HOME_PAGE_SOURCE = '/'
DEFAULT_SETTINGS = {  # As delivered by configparser to this parser
    'pgwui.home_page': f'type = {DEFAULT_HOME_PAGE_TYPE}\n'
                       f'source = {DEFAULT_HOME_PAGE_SOURCE}\n'}

# Logging
log = logging.getLogger(__name__)


# Functions

def dot_to_dict(settings, key, new_key):
    settings['pgwui'][new_key] = settings[key]
    del settings[key]


def parse_multiline_assignments(lines, result):
    '''Add the parse value to the result
    '''
    for line in lines.splitlines():
        if '=' in line:
            key, val = line.split('=', 1)
            result.append((key.rstrip(), val.lstrip()))
        else:
            stripped = line.lstrip()
            if stripped != '':
                # Multiple values on different lines means a list
                try:
                    key, val = result[-1]
                except IndexError:
                    raise server_ex.MissingEqualError(stripped)
                if not isinstance(val, list):
                    val = [val]
                val.append(stripped)
                result[-1] = (key, val)


def parse_assignments(lines):
    '''Return a list of key/value tuples from the lines of a setting
    '''
    result = []
    if isinstance(lines, str):
        parse_multiline_assignments(lines, result)
    else:
        for key, val in lines.items():
            result.append((key, val))
    return result


def dot_to_multiline_setting(settings, key, pgwui_key):
    '''Put a multi-line setting into its own dict,
    adding to what's already there
    '''
    multi_setting = settings['pgwui'].setdefault(pgwui_key, dict())
    try:
        multi_setting.update(dict(parse_assignments(settings[key])))
    except server_ex.MissingEqualError:
        raise
    finally:
        del settings[key]


def component_setting_into_dict(
        errors, component_checkers, key, settings, component):
    '''Put a component's settings in its own dict and validate them
    '''
    try:
        dot_to_multiline_setting(settings, key, component)
    except server_ex.MissingEqualError as ex:
        # Couldn't get the settings because there's no "="
        errors.append(server_ex.BadValueError(f'pgwui:{component}', ex))
        return
    if component in component_checkers:
        errors.extend(
            component_checkers[component](settings['pgwui'][component]))


def setting_into_dict(
        errors, components, component_checkers, key, settings):
    '''Separate a pgwui setting into a dict on '.' chars; validate
    component settings.
    '''
    if key[:6] == 'pgwui.':
        new_key = key[6:]
        if new_key in components:
            component_setting_into_dict(
                errors, component_checkers, key, settings, new_key)
        else:
            if new_key in SETTINGS:
                dot_to_dict(settings, key, new_key)
            elif new_key in MULTI_SETTINGS:
                try:
                    dot_to_multiline_setting(settings, key, new_key)
                except server_ex.MissingEqualError as ex:
                    errors.append(
                        server_ex.BadValueError(f'pgwui:{new_key}', ex))
            else:
                errors.append(common_ex.UnknownSettingKeyError(key))


def dictify_settings(errors, settings, components):
    '''Convert "." in the pgwui settings to dict mappings, and validate
    the result.
    '''
    component_checkers = plugin.find_pgwui_check_settings()
    settings.setdefault('pgwui', dict())
    for key in list(settings.keys()):
        setting_into_dict(
            errors, components, component_checkers, key, settings)


def exit_reporting_errors(errors):
    '''Report errors and exit
    '''
    tagged = [(logging.ERROR, error) for error in errors]
    tagged.append((logging.CRITICAL, server_ex.BadSettingsAbort()))

    for (level, error) in tagged:
        log.log(level, error)

    for (level, error) in (tagged[0], tagged[-1]):
        print(error, file=sys.stderr)    # in case logging is broken

    sys.exit(1)


def add_default_settings(settings):
    '''Add the default settings to the config if not there
    '''
    for setting, val in DEFAULT_SETTINGS.items():
        settings.setdefault(setting, val)


def exit_on_invalid_settings(settings, components):
    '''Exit when settings don't validate
    '''
    add_default_settings(settings)
    errors = []
    dictify_settings(errors, settings, components)
    check_settings.validate_settings(errors, settings)
    if errors:
        exit_reporting_errors(errors)


def autoconfigurable_components(settings, components):
    '''Automatic pgwui component discovery
    '''
    autoconfig = settings['pgwui'].get('autoconfigure', True)
    if not autoconfig:
        return []

    if 'pyramid.include' in settings:
        log.info(server_ex.AutoconfigureConflict())

    return components


def in_development(settings):
    '''Boolean: Whether or not the pyramid testing pack is installed
    '''
    pyramid_includes = settings.get('pyramid.includes', [])
    if isinstance(pyramid_includes, str):
        if '\n' in pyramid_includes:
            pyramid_includes = pyramid_includes.splitlines()
        else:
            pyramid_includes = pyramid_includes.split(' ')
    result = 'pyramid_debugtoolbar' in pyramid_includes
    log.debug(
        f'pyramid_debugtoolbar included: {result}  '
        f'So introspection set to: {result}')
    return result


def apply_component_defaults(settings, components):
    '''Apply component default settings to existing settings
    '''
    introspection = in_development(settings)
    with Configurator(settings=settings,
                      introspection=introspection) as config:
        config.include('pgwui_common')

        components_to_config = autoconfigurable_components(
            settings, components)
        rp = settings['pgwui'].get('route_prefix')
        with config.route_prefix_context(rp):
            for component in components_to_config:
                log.debug(
                    'Autoconfiguring PGWUI component: {}'.format(component))
                config.include(component)
            routes.add_routes(config, settings)
        log.debug('Done autoconfiguring PGWUI components')
        errors = assets.override_assets(config, settings)
        return (config, errors)


def pgwui_server_config(settings):
    '''Configure pyramid
    '''
    components = plugin.find_pgwui_components()
    exit_on_invalid_settings(settings, components)
    try:
        (config, errors) = apply_component_defaults(settings, components)
    except pyramid.exceptions.ConfigurationError as exp:
        exit_reporting_errors([common_ex.BadSettingError(exp)])
    errors.extend(pgwui_common.urls.add_urls_setting(config, settings))
    if errors:
        exit_reporting_errors(errors)
    return config


def main(global_config, **settings):
    '''Return a Pyramid WSGI application
    '''
    config = pgwui_server_config(settings)
    return config.make_wsgi_app()
