# BFH Science Self-Service Portal
# Copyright (C) 2025-2026 IKUS Software
#
# 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 more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.

from datetime import datetime, timezone
from functools import partial
from importlib.resources import files

import cherrypy
import cherrypy_foundation.plugins.ldap  # noqa
import cherrypy_foundation.plugins.smtp  # noqa
import cherrypy_foundation.tools.auth  # noqa
import cherrypy_foundation.tools.jinja2  # noqa: import cherrypy.tools.jinja2
import cherrypy_foundation.tools.ratelimit
import cherrypy_foundation.tools.secure_headers  # noqa: import cherrypy.tools.secure_headers
import cherrypy_foundation.tools.sessions_timeout  # noqa
from cherrypy import Application
from cherrypy_foundation.error_page import error_page
from cherrypy_foundation.flash import get_flashed_messages
from cherrypy_foundation.sessions import FileSession
from cherrypy_foundation.url import url_for

import bfh_science_ssp.core.microsoft_mfa  # noqa
import bfh_science_ssp.core.notification  # noqa
import bfh_science_ssp.core.openstack  # noqa
import bfh_science_ssp.core.znuny  # noqa
from bfh_science_ssp.controller import template_processor
from bfh_science_ssp.controller.dashboard_page import DashboardPage
from bfh_science_ssp.controller.language import Language
from bfh_science_ssp.controller.login_page import LoginPage
from bfh_science_ssp.controller.logout_page import LogoutPage
from bfh_science_ssp.controller.static import Static
from bfh_science_ssp.core.user import User

SESSION_USER_KEY = 'username'

# Define cherrypy development environment
cherrypy.config.environments['development'] = {
    'engine.autoreload.on': True,
    'checker.on': False,
    'tools.log_headers.on': True,
    'request.show_tracebacks': True,
    'request.show_mismatched_params': True,
    'log.screen': False,
}


def user_lookup_func(login, user_info=None):
    """
    This function search a user for the given login name.
    """
    if user_info and 'dn' in user_info:
        user = User(dn=user_info['dn'], attrs=user_info)
        return login, user
    return False


def user_from_key_func(login):
    """
    This function is used to load current user data.
    """
    return User.from_username(login)


@cherrypy.tools.auth(
    session_user_key=SESSION_USER_KEY,
    user_lookup_func=user_lookup_func,
    user_from_key_func=user_from_key_func,
    checkpassword=[cherrypy.ldap.authenticate],
)
@cherrypy.tools.i18n()
@cherrypy.tools.proxy(local=None, remote='X-Real-IP')
@cherrypy.tools.ratelimit(on=False, session_user_key=SESSION_USER_KEY)
@cherrypy.tools.secure_headers(
    csp={
        "default-src": ["'self'"],
        "script-src": ["'self'", "'unsafe-inline'"],
        "style-src": ["'self'", "'unsafe-inline'"],
        "img-src": ["'self'", "data:"],
        "font-src": ["'self'"],
    }
)
@cherrypy.tools.sessions()
@cherrypy.tools.sessions_timeout()
class Root(object):
    """
    Root entry point exposed using cherrypy.
    """

    def __init__(self):
        self.dashboard = DashboardPage()
        self.login = LoginPage()
        self.logout = LogoutPage()
        self.static = Static()
        self.language = Language()

    favicon_ico = Static.favicon_ico
    favicon_ico._cp_config = Static._cp_config

    @cherrypy.expose
    @cherrypy.tools.jinja2(template='index.html')
    def index(self):
        raise cherrypy.HTTPRedirect(url_for('dashboard', ''))


class SspApplication(Application):
    def __init__(self, cfg):
        self.cfg = cfg

        # Configure Jinja2 environment.
        env = cherrypy.tools.jinja2.create_env(
            package_name='bfh_science_ssp',
            globals={
                # Configuration
                'footer_name': cfg.footer_name,
                'footer_url': cfg.footer_url,
                'get_flashed_messages': get_flashed_messages,
                'header_name': cfg.header_name,
                'openstack_enabled': bool(cfg.openstack_auth_url),
                'url_for': url_for,
                'version': bfh_science_ssp.__version__,
                'now': partial(datetime.now, tz=timezone.utc),
            },
        )

        # Pick the right implementation for storage
        rate_limit_storage_class = cherrypy_foundation.tools.ratelimit.RamRateLimit
        session_storage_class = cherrypy.lib.sessions.RamSession
        if cfg.session_dir:
            rate_limit_storage_class = cherrypy_foundation.tools.ratelimit.FileRateLimit
            session_storage_class = FileSession
        cherrypy.config.update(
            {
                # Define cherrypy config based on debug flag.
                'environment': 'development' if cfg.debug else 'production',
                # Define error page handler.
                'error_page.default': error_page,
                # Configure session storage
                'tools.sessions.debug': cfg.debug,
                'tools.sessions.locking': 'explicit',
                'tools.sessions.storage_class': session_storage_class,
                'tools.sessions.storage_path': cfg.session_dir,
                'tools.sessions.httponly': True,
                'tools.sessions.timeout': cfg.session_idle_timeout,  # minutes
                'tools.sessions.persistent': False,
                'tools.sessions_timeout.persistent_timeout': cfg.session_persistent_timeout,  # minutes
                'tools.sessions_timeout.absolute_timeout': cfg.session_absolute_timeout,  # minutes
                # Configure rate limit
                'tools.ratelimit.debug': cfg.debug,
                'tools.ratelimit.delay': cfg.rate_limit_delay,
                'tools.ratelimit.limit': cfg.rate_limit,
                'tools.ratelimit.storage_class': rate_limit_storage_class,
                'tools.ratelimit.storage_path': cfg.session_dir,
                # Configure jinja2 templating engine
                'tools.jinja2.env': env,
                'tools.jinja2.extra_processor': template_processor,
                # Configure locales
                'tools.i18n.default': cfg.default_lang,
                'tools.i18n.default_timezone': cfg.default_timezone,
                'tools.i18n.mo_dir': files('bfh_science_ssp') / 'locales',
                'tools.i18n.domain': 'messages',
                'tools.i18n.cookie_name': 'locale',
                # Configure LDAP plugin
                'ldap.uri': cfg.ldap_uri,
                'ldap.base_dn': cfg.ldap_base_dn,
                'ldap.bind_dn': cfg.ldap_bind_dn,
                'ldap.bind_password': cfg.ldap_bind_password,
                'ldap.scope': cfg.ldap_scope,
                'ldap.tls': cfg.ldap_tls,
                'ldap.login_attribute': cfg.ldap_login_attribute,
                # FIXME username_attribute: for backward compatibility with cherrypy-foundation <= 1.0.0a21
                'ldap.username_attribute': cfg.ldap_login_attribute,
                'ldap.user_filter': cfg.ldap_user_filter,
                'ldap.group_filter': cfg.ldap_group_filter,
                'ldap.group_attribute': cfg.ldap_group_attribute,
                'ldap.group_attribute_is_dn': cfg.ldap_group_attribute_is_dn,
                'ldap.version': cfg.ldap_version,
                'ldap.network_timeout': cfg.ldap_network_timeout,
                'ldap.timeout': cfg.ldap_timeout,
                'ldap.encoding': cfg.ldap_encoding,
                'ldap.fullname_attribute': cfg.ldap_fullname_attribute,
                'ldap.firstname_attribute': cfg.ldap_firstname_attribute,
                'ldap.lastname_attribute': cfg.ldap_lastname_attribute,
                'ldap.email_attribute': cfg.ldap_email_attribute,
                'ldap.pool_size': cfg.ldap_pool_size,
                # Configure SMTP plugin
                'smtp.server': cfg.smtp_server,
                'smtp.username': cfg.smtp_username,
                'smtp.password': cfg.smtp_password,
                'smtp.email_from': cfg.smtp_from and '%s <%s>' % (cfg.header_name.striptags(), cfg.smtp_from),
                'smtp.encryption': cfg.smtp_encryption,
                'smtp.bcc': cfg.smtp_bcc,
                # Configure notification
                'notification.env': env,
                'notification.header_name': cfg.header_name,
                # Configure Znuny
                'znuny.userlogin': cfg.znuny_userlogin,
                'znuny.password': cfg.znuny_password,
                'znuny.ticket_create_url': cfg.znuny_ticket_create_url,
                # Configure openstack
                'openstack.username': cfg.openstack_username,
                'openstack.password': cfg.openstack_password,
                'openstack.auth_url': cfg.openstack_auth_url,
                # Configure Microsoft MFA
                'microsoft_mfa.tenant_id': cfg.microsoft_tenant_id,
                'microsoft_mfa.client_id': cfg.microsoft_client_id,
                'microsoft_mfa.client_secret': cfg.microsoft_client_secret,
                'microsoft_mfa.client_private_key': cfg.microsoft_client_private_key,
                'microsoft_mfa.client_cert_thumbprint': cfg.microsoft_client_cert_thumbprint,
            }
        )

        # Initialize the application
        Application.__init__(self, root=Root())
