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

# This file is part of PGWUI_Common.
#
# 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>

'''Validate PGWUI_Core and PGWUI_Common configuration
'''

import re
from ast import literal_eval

from . import constants
from . import exceptions as ex
from . import checkset


# Regular expressions for page "source" values, by type
URL_RE = re.compile('^(?:(?:[^:/]+:)?//[^/])|(?:/(?:[^/]|$))')


def key_to_ini(key):
    '''Convert the setting key to a key used in an ini file's declaration
    '''
    return 'pgwui:{}'.format(key)


def require_setting(errors, setting, pgwui_settings, formatter):
    if setting not in pgwui_settings:
        errors.append(ex.MissingSettingError(formatter(setting)))
        return False
    return True


def boolean_setting(errors, setting, pgwui_settings):
    if setting in pgwui_settings:
        try:
            val = literal_eval(pgwui_settings[setting])
        except ValueError:
            val = None
        if (val is not True
                and val is not False):
            errors.append(ex.NotBooleanSettingError(
                key_to_ini(setting), pgwui_settings[setting]))


def validate_setting_values(errors, settings):
    '''Check each settings value for validity
    '''
    pgwui_settings = settings['pgwui']

    # pg_host can be missing, it defaults to the Unix socket (in psycopg2)

    # pg_port can be missing, it defaults to 5432 (in psycopg2)

    # default_db can be missing, then the user sees no default

    # dry_run
    require_setting(errors, 'dry_run', pgwui_settings, key_to_ini)
    boolean_setting(errors, 'dry_run', pgwui_settings)

    # route_prefix can be missing, defaults to no route prefix which is fine.

    # routes can be missing, sensible defaults are built-in.

    # validate_hmac
    boolean_setting(errors, 'validate_hmac', pgwui_settings)


def do_validate_hmac(settings):
    '''True unless the user has specificly rejected hmac validation
    '''
    pgwui_settings = settings['pgwui']
    return ('validate_hmac' not in pgwui_settings
            or literal_eval(pgwui_settings['validate_hmac']))


def validate_hmac(errors, settings):
    '''Unless otherwise requested, validate the session.secret setting'''
    if not do_validate_hmac(settings):
        return

    if 'session.secret' not in settings:
        errors.append(ex.NoHMACError())
        return

    if len(settings['session.secret']) != constants.HMAC_LEN:
        errors.append(ex.HMACLengthError())
        return


def page_key_to_ini(page_key, subkey):
    '''Convert the page setting subkey to a ini file declaration
    '''
    return key_to_ini(f'{page_key}:{subkey}')


def require_page_settings(errors, required_settings, page_settings, page_key):
    '''Check for required keys in the page setting
    '''
    def subkey_to_ini(subkey):
        return page_key_to_ini(page_key, subkey)

    have_settings = True
    for subkey in required_settings:
        have_settings &= require_setting(
            errors, subkey, page_settings, subkey_to_ini)

    return have_settings


def validate_url_source(errors, page_key, source):
    '''Validate the page setting "source" for URLs
    '''
    if URL_RE.match(source):
        return
    errors.append(ex.BadURLSourceError(
        page_key_to_ini(page_key, 'source'), source))


def validate_url_path(errors, page_key, page_settings):
    '''Validate the page setting "url_path"
    '''
    url_path = page_settings['url_path']
    if url_path[0:1] == '/':
        return
    errors.append(ex.BadFileURLPathError(
        page_key_to_ini(page_key, 'url_path'), url_path))


def validate_file_source(errors, page_key, source):
    '''Validate the page setting "source" for files
    '''
    if source[0:1] == '/':
        return
    errors.append(ex.BadFileSourceError(
        page_key_to_ini(page_key, 'file'), source))


def validate_route_source(errors, page_key, source):
    '''Validate the page setting "source" for routes

    The routes are not yet established, so we don't confirm
    existance at this point.
    '''
    if source != '':
        return
    errors.append(ex.BadRouteSourceError(
        page_key_to_ini(page_key, 'route'), source))


def validate_asset_source(errors, page_key, source):
    '''Validate the page setting "source" for assets
    '''
    if source != '':
        return
    errors.append(ex.BadAssetSourceError(
        page_key_to_ini(page_key, 'asset'), source))


def validate_file_content(errors, page_key, page_settings, source):
    '''Validate the content of a "file" page setting
    '''
    validate_file_source(errors, page_key, source)
    if require_page_settings(
            errors, ['url_path'], page_settings, page_key):
        validate_url_path(errors, page_key, page_settings)
    errors.extend(checkset.unknown_settings(
        f'pgwui:{page_key}', ['type', 'source', 'url_path'], page_settings))


def validate_type_content(errors, page_key, page_settings):
    '''Validate the page setting's "type", and other page setting content
    based on the type
    '''
    type = page_settings['type']
    source = page_settings['source']
    if type == 'URL':
        validate_url_source(errors, page_key, source)
        errors.extend(checkset.unknown_settings(
            'pgwui_common', ['type', 'source'], page_settings))
        return
    if type == 'file':
        validate_file_content(errors, page_key, page_settings, source)
        return
    if type == 'route':
        validate_route_source(errors, page_key, source)
        errors.extend(checkset.unknown_settings(
            'pgwui_common', ['type', 'source'], page_settings))
        return
    if type == 'asset':
        validate_asset_source(errors, page_key, source)
        errors.extend(checkset.unknown_settings(
            'pgwui_common', ['type', 'source'], page_settings))
        return

    errors.append(ex.BadPageTypeError(
        page_key_to_ini(page_key, 'type'), type))


def validate_page_setting(errors, settings, page_key):
    '''Validate the multiple values of the page setting
    '''
    pgwui_settings = settings['pgwui']
    if page_key not in pgwui_settings:
        return

    page_settings = pgwui_settings[page_key]
    if not require_page_settings(
            errors, ['type', 'source'], page_settings, page_key):
        return

    validate_type_content(errors, page_key, page_settings)


def validate_settings(errors, settings):
    '''Validate all core settings
    '''
    validate_setting_values(errors, settings)
    validate_hmac(errors, settings)
    validate_page_setting(errors, settings, 'home_page')
    validate_page_setting(errors, settings, 'menu_page')
