# Copyright (C) 2012-2024 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
# (only), as published by the Free Software Foundation.
#
# 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 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/>.
#
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/

"""
RhodeCode authentication library for PAM
"""

import colander
import grp
import logging
import pam
import pwd
import re
import socket

from rhodecode.translation import _
from rhodecode.authentication.base import RhodeCodeExternalAuthPlugin, hybrid_property
from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase, TwoFactorAuthnPluginSettingsSchemaMixin
from rhodecode.authentication.routes import AuthnPluginResourceBase
from rhodecode.lib.colander_utils import strip_whitespace

log = logging.getLogger(__name__)


def plugin_factory(plugin_id, *args, **kwargs):
    """
    Factory function that is called during plugin discovery.
    It returns the plugin instance.
    """
    plugin = RhodeCodeAuthPlugin(plugin_id)
    return plugin


class PamAuthnResource(AuthnPluginResourceBase):
    pass


class PamSettingsSchema(TwoFactorAuthnPluginSettingsSchemaMixin, AuthnPluginSettingsSchemaBase):
    service = colander.SchemaNode(
        colander.String(),
        default="login",
        description=_("PAM service name to use for authentication."),
        preparer=strip_whitespace,
        title=_("PAM service name"),
        widget="string",
    )
    gecos = colander.SchemaNode(
        colander.String(),
        default=r"(?P<last_name>.+),\s*(?P<first_name>\w+)",
        description=_("Regular expression for extracting user name/email etc. from Unix userinfo."),
        preparer=strip_whitespace,
        title=_("Gecos Regex"),
        widget="string",
    )


class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
    uid = "pam"
    # PAM authentication can be slow. Repository operations involve a lot of
    # auth calls. Little caching helps speedup push/pull operations significantly
    AUTH_CACHE_TTL = 4

    def includeme(self, config):
        config.add_authn_plugin(self)
        config.add_authn_resource(self.get_id(), PamAuthnResource(self))
        config.add_view(
            "rhodecode.authentication.views.AuthnPluginViewBase",
            attr="settings_get",
            renderer="rhodecode:templates/admin/auth/plugin_settings.mako",
            request_method="GET",
            route_name="auth_home",
            context=PamAuthnResource,
        )
        config.add_view(
            "rhodecode.authentication.views.AuthnPluginViewBase",
            attr="settings_post",
            renderer="rhodecode:templates/admin/auth/plugin_settings.mako",
            request_method="POST",
            route_name="auth_home",
            context=PamAuthnResource,
        )

    def get_display_name(self, load_from_settings=False):
        return _("PAM")

    @classmethod
    def docs(cls):
        return "https://docs.rhodecode.com/RhodeCode-Enterprise/auth/auth-pam.html"

    @hybrid_property
    def name(self):
        return "pam"

    def get_settings_schema(self):
        return PamSettingsSchema()

    def use_fake_password(self):
        return True

    def auth(self, userobj, username, password, settings, **kwargs):
        if not username or not password:
            log.debug("Empty username or password skipping...")
            return None
        _pam = pam.pam()
        auth_result = _pam.authenticate(username, password, settings["service"])

        if not auth_result:
            log.error("PAM was unable to authenticate user: %s", username)
            return None

        log.debug("Got PAM response %s", auth_result)

        # old attrs fetched from RhodeCode database
        default_email = "{}@{}".format(username, socket.gethostname())
        admin = getattr(userobj, "admin", False)
        active = getattr(userobj, "active", True)
        email = getattr(userobj, "email", "") or default_email
        username = getattr(userobj, "username", username)
        firstname = getattr(userobj, "firstname", "")
        lastname = getattr(userobj, "lastname", "")
        extern_type = getattr(userobj, "extern_type", "")

        user_attrs = {
            "username": username,
            "firstname": firstname,
            "lastname": lastname,
            "groups": [g.gr_name for g in grp.getgrall() if username in g.gr_mem],
            "user_group_sync": True,
            "email": email,
            "admin": admin,
            "active": active,
            "active_from_extern": None,
            "extern_name": username,
            "extern_type": extern_type,
        }

        try:
            user_data = pwd.getpwnam(username)
            regex = settings["gecos"]
            match = re.search(regex, user_data.pw_gecos)
            if match:
                user_attrs["firstname"] = match.group("first_name")
                user_attrs["lastname"] = match.group("last_name")
        except Exception:
            log.warning("Cannot extract additional info for PAM user")
            pass

        log.debug("pamuser: %s", user_attrs)
        log.info(
            "user `%s` authenticated correctly",
            user_attrs["username"],
            extra={"action": "user_auth_ok", "auth_module": "auth_pam", "username": user_attrs["username"]},
        )
        return user_attrs


def includeme(config):
    plugin_id = f"egg:rhodecode-enterprise-ce#{RhodeCodeAuthPlugin.uid}"
    plugin_factory(plugin_id).includeme(config)
