# -*- coding: utf-8 -*-
#
#    Authors:
#        Pavel Březina <pbrezina@redhat.com>
#
#    Copyright (C) 2018 Red Hat
#
#    This program is free software; you can redistribute it and/or modify
#    it under the terms of the GNU 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 General Public License for morerequi details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
#

import argparse
import gettext

_ = gettext.gettext


class Option:
    def __init__(self, name, metavar, help, feature, supported):
        self.name = name
        self.metavar = metavar
        self.help = help
        self.feature = feature
        self.supported = supported
        self.value = None
        self.from_sysconfig = False

    def set(self, new_value):
        self.value = new_value

    def set_from_sysconfig(self, new_value):
        self.set(new_value)
        self.from_sysconfig = True

    def isset(self):
        return self.value is not None

    @staticmethod
    def Valued(name, metavar, help):
        return Option(name, metavar, help, feature=False, supported=True)

    @staticmethod
    def Switch(name, help):
        return Option(name, None, help, feature=False, supported=True)

    @staticmethod
    def Feature(name, help):
        return Option(name, None, help, feature=True, supported=True)

    @staticmethod
    def UnsupportedValued(name, metavar):
        return Option(name, metavar, None, feature=False, supported=False)

    @staticmethod
    def UnsupportedFeature(name):
        return Option(name, None, None, feature=True, supported=False)

    @staticmethod
    def UnsupportedSwitch(name):
        return Option(name, None, None, feature=False, supported=False)


class Options:
    List = [
        # These options are still supported in authconfig compatibility
        # layers. The tool will do its best to translate them to authselect
        # call and where needed, it will generate a configuration file.
        #
        # However, they will just make sure that an authentication against
        # expected service is working. They may not result in the exact same
        # configuration as authconfig would generate.
        Option.Feature("nis",             _("NIS for user information by default")),
        Option.Valued ("nisdomain",       _("<domain>"), _("default NIS domain")),
        Option.Valued ("nisserver",       _("<server>"), _("default NIS server")),
        Option.Feature("ldap",            _("LDAP for user information by default")),
        Option.Feature("ldapauth",        _("LDAP for authentication by default")),
        Option.Valued ("ldapserver",      _("<server>"), _("default LDAP server hostname or URI")),
        Option.Valued ("ldapbasedn",      _("<dn>"), _("default LDAP base DN")),
        Option.Feature("ldaptls",         _("use of TLS with LDAP (RFC-2830)")),
        Option.Feature("ldapstarttls",    _("use of TLS for identity lookups with LDAP (RFC-2830)")),
        Option.Feature("rfc2307bis",      _("use of RFC-2307bis schema for LDAP user information lookups")),
        Option.Feature("smartcard",       _("authentication with smart card by default")),
        Option.Valued ("smartcardaction", _("<0=Lock|1=Ignore>"), _("action to be taken on smart card removal")),
        Option.Feature("requiresmartcard",_("require smart card for authentication by default")),
        Option.Feature("fingerprint",     _("authentication with fingerprint readers by default")),
        Option.Feature("ecryptfs",        _("automatic per-user ecryptfs")),
        Option.Feature("krb5",            _("Kerberos authentication by default")),
        Option.Valued ("krb5kdc",         _("<server>"), _("default Kerberos KDC")),
        Option.Valued ("krb5adminserver", _("<server>"), _("default Kerberos admin server")),
        Option.Valued ("krb5realm",       _("<realm>"), _("default Kerberos realm")),
        Option.Feature("krb5kdcdns",      _("use of DNS to find Kerberos KDCs")),
        Option.Feature("krb5realmdns",    _("use of DNS to find Kerberos realms")),
        Option.Feature("winbind",         _("winbind for user information by default")),
        Option.Feature("winbindauth",     _("winbind for authentication by default")),
        Option.Valued ("winbindjoin",     _("<Administrator>"), _("join the winbind domain or ads realm now as this administrator")),
        Option.Feature("winbindkrb5",     _("Kerberos 5 for authenticate with winbind")),
        Option.Valued ("smbworkgroup",    _("<workgroup>"), _("workgroup authentication servers are in")),
        Option.Feature("sssd",            _("SSSD for user information by default with manually managed configuration")),
        Option.Feature("sssdauth",        _("SSSD for authentication by default with manually managed configuration")),
        Option.Feature("cachecreds",      _("caching of user credentials in SSSD by default")),
        Option.Feature("pamaccess",       _("check of access.conf during account authorization")),
        Option.Feature("mkhomedir",       _("creation of home directories for users on their first login")),
        Option.Feature("faillock",        _("account locking in case of too many consecutive authentication failures")),
        Option.Valued ("passminlen",      _("<number>"), _("minimum length of a password")),
        Option.Valued ("passminclass",    _("<number>"), _("minimum number of character classes in a password")),
        Option.Valued ("passmaxrepeat",   _("<number>"), _("maximum number of same consecutive characters in a password")),
        Option.Valued ("passmaxclassrepeat", _("<number>"), _("maximum number of consecutive characters of same class in a password")),
        Option.Feature("reqlower",        _("require at least one lowercase character in a password")),
        Option.Feature("requpper",        _("require at least one uppercase character in a password")),
        Option.Feature("reqdigit",        _("require at least one digit in a password")),
        Option.Feature("reqother",        _("require at least one other character in a password")),

        # Program options
        Option.Switch ("nostart",         _("do not start/stop services")),
        Option.Switch ("updateall",       _("update all configuration files")),
        Option.Switch ("update",          _("the same as --updateall")),
        Option.Switch ("kickstart",       _("the same as --updateall")),

        # Hidden compat tool option, useful for testing. No changes to the
        # system will be done, they will be printed.
        Option.Switch ("test-call",       argparse.SUPPRESS),

        # Unsupported program options but we have to react somehow when set
        Option.UnsupportedSwitch("test"),
        Option.UnsupportedSwitch("probe"),
        Option.UnsupportedValued("savebackup", _("<name>")),
        Option.UnsupportedValued("restorebackup", _("<name>")),
        Option.UnsupportedSwitch("restorelastbackup"),

        # These options are no longer supported in authconfig compatibility
        # layers and will produce warning when used. They will not affect
        # the system.
        Option.UnsupportedFeature("cache"),
        Option.UnsupportedFeature("shadow"),
        Option.UnsupportedSwitch ("useshadow"),
        Option.UnsupportedFeature("md5"),
        Option.UnsupportedSwitch ("usemd5"),
        Option.UnsupportedValued ("passalgo", _("<descrypt|bigcrypt|md5|sha256|sha512>")),
        Option.UnsupportedValued ("ldaploadcacert", _("<URL>")),
        Option.UnsupportedValued ("smartcardmodule", _("<module>")),
        Option.UnsupportedValued ("smbsecurity", _("<user|server|domain|ads>")),
        Option.UnsupportedValued ("smbrealm", _("<realm>")),
        Option.UnsupportedValued ("smbservers", _("<servers>")),
        Option.UnsupportedValued ("smbidmaprange", _("<lowest-highest>")),
        Option.UnsupportedValued ("smbidmapuid", _("<lowest-highest>")),
        Option.UnsupportedValued ("smbidmapgid", _("<lowest-highest>")),
        Option.UnsupportedValued ("winbindseparator", _("<\>")),
        Option.UnsupportedValued ("winbindtemplatehomedir", _("</home/%D/%U>")),
        Option.UnsupportedValued ("winbindtemplateshell", _("</bin/false>")),
        Option.UnsupportedFeature("winbindusedefaultdomain"),
        Option.UnsupportedFeature("winbindoffline"),
        Option.UnsupportedFeature("preferdns"),
        Option.UnsupportedFeature("forcelegacy"),
        Option.UnsupportedFeature("locauthorize"),
        Option.UnsupportedFeature("sysnetauth"),
        Option.UnsupportedValued ("faillockargs", _("<options>")),
    ]

    Map = {
        # These options were use with autodetection of pam_cracklib
        # and pam_passwdqc. However, authselect supports only pam_pwquality.
        # "USEPWQUALITY"     : "",
        # "USEPASSWDQC"      : "",
        "USEFAILLOCK"      : "faillock",
        "FAILLOCKARGS"     : "faillockargs",
        "USELDAP"          : "ldap",
        "USENIS"           : "nis",
        "USEECRYPTFS"      : "ecryptfs",
        "USEWINBIND"       : "winbind",
        "WINBINDKRB5"      : "winbindkrb5",
        "USESSSD"          : "sssd",
        "USEKERBEROS"      : "krb5",
        "USELDAPAUTH"      : "ldapauth",
        "USESMARTCARD"     : "smartcard",
        "FORCESMARTCARD"   : "requiresmartcard",
        "USEFPRINTD"       : "fingerprint",
        "PASSWDALGORITHM"  : "passalgo",
        "USEMD5"           : "md5",
        "USESHADOW"        : "shadow",
        "USEWINBINDAUTH"   : "winbindauth",
        "USESSSDAUTH"      : "sssdauth",
        "USELOCAUTHORIZE"  : "locauthorize",
        "USEPAMACCESS"     : "pamaccess",
        "USEMKHOMEDIR"     : "mkhomedir",
        "USESYSNETAUTH"    : "sysnetauth",
        "FORCELEGACY"      : "forcelegacy",
        "CACHECREDENTIALS" : "cachecreds",
    }

    def __init__(self):
        self.options = {}

        for option in self.List:
            self.options[option.name] = option

    def parse(self):
        parser = argparse.ArgumentParser(description='Authconfig Compatibility Tool.')

        parsers = {
            'supported'   : parser.add_argument_group(_('These options have a compatibility layer')),
            'unsupported' : parser.add_argument_group(_('These options are no longer supported and have no effect'))
        }

        for option in self.List:
            group = 'supported' if option.supported else 'unsupported'
            self.add_option(parsers[group], option)

        cmdline = parser.parse_args()

        for name, option in self.options.items():
            value = getattr(cmdline, name)
            option.set(value)

        # usemd5 and useshadow are equivalent to enablemd5 and enableshadow
        if not self.isset('md5') and self.isset('usemd5'):
            self.set('md5', self.get('usemd5'))

        if not self.isset('shadow') and self.isset('useshadow'):
            self.set('shadow', self.get('useshadow'))

        # ldapstarttls is equivalent to ldaptls
        if self.isset('ldapstarttls') and not self.isset('ldaptls'):
            self.set('ldaptls', self.get('ldapstarttls'))

    def applysysconfig(self, sysconfig):
        for name, option in self.Map.items():
            if not self.isset(option):
                self.options[option].set_from_sysconfig(sysconfig.get(name))

    def updatesysconfig(self, sysconfig):
        for name, option in self.Map.items():
            if self.isset(option):
                sysconfig.set(name, self.get(option))

    def get(self, name):
        return self.options[name].value

    def set(self, name, value):
        self.options[name].set(value)

    def isset(self, name):
        return self.options[name].isset()

    def getBool(self, name):
        value = self.get(name)
        if value is None or not value:
            return False
        return True

    def getTrueOrNone(self, name):
        value = self.get(name)
        if value is None or not value:
            return None
        return True

    def getSetButUnsupported(self):
        options = []
        for option in Options.List:
            if option.supported:
                continue

            if not option.isset():
                continue

            if option.from_sysconfig:
                continue

            name = option.name
            if option.feature:
                name = "enable" + name if option.value else "disable" + name

            options.append(name)

        return options

    def add_option(self, parser, option):
        if option.metavar is not None:
            self.add_valued(parser, option)
        elif option.feature:
            self.add_feature(parser, option)
        else:
            self.add_switch(parser, option)

    def add_valued(self, parser, option):
        parser.add_argument("--"+option.name,
                            action='store',
                            help=option.help,
                            dest=option.name,
                            metavar=option.metavar)

    def add_switch(self, parser, option):
        parser.add_argument("--"+option.name,
                            action='store_const',
                            const=True,
                            help=option.help,
                            dest=option.name)

    def add_feature(self, parser, option):
        help_enable = None
        help_disable = None

        if option.help is not None:
            help_enable = _("enable") + " " + option.help
            help_disable = _("disable") + " " + option.help

        parser.add_argument("--enable" + option.name,
                            action='store_const',
                            const=True,
                            help=help_enable,
                            dest=option.name)

        parser.add_argument("--disable" + option.name,
                            action='store_const',
                            const=False,
                            help=help_disable,
                            dest=option.name)
