# 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/>.

import logging
from urllib.parse import quote

import cherrypy
import msal
import requests
from cherrypy.process.plugins import SimplePlugin
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

import bfh_science_ssp

logger = logging.getLogger(__name__)

# ref: https://learn.microsoft.com/en-us/graph/api/resources/authenticationmethods-overview
MFA_TYPE = [
    "#microsoft.graph.microsoftAuthenticatorAuthenticationMethod",
    "#microsoft.graph.phoneAuthenticationMethod",
    "#microsoft.graph.fido2AuthenticationMethod",
]


class MicrosoftMfaPlugin(SimplePlugin):
    """
    Plugin to query Microsoft Graph API to retrieve users MFA status.
    """

    tenant_id = None
    client_id = None
    client_secret = None
    client_private_key = None
    client_cert_thumbprint = None

    _session = None
    _conn = None

    def start(self):
        # If tenant ID is not defined, skip this plugin.
        if not self.tenant_id:
            return
        self.bus.log('Start Microsoft MFA plugin')
        if not self.client_id:
            raise ValueError('client_id is required for Microsoft MFA plugin')
        if not self.client_secret and not self.client_private_key:
            raise ValueError('either client_secret or client_private_key is required for Microsoft MFA plugin')
        if self.client_private_key and not self.client_cert_thumbprint:
            raise ValueError('certificate thumbprint required with client_private_key')

        # Authenticate with Microsoft
        self._conn = msal.ConfidentialClientApplication(
            client_id=self.client_id,
            client_credential=(
                {'private_key': self.client_private_key, 'thumbprint': self.client_cert_thumbprint}
                if self.client_private_key
                else self.client_secret
            ),
            authority=f"https://login.microsoftonline.com/{self.tenant_id}",
        )
        # Get a token to test authentication.
        self._get_bearer_token()

        # Configure HTTP connection pool.
        self._session = requests.Session()
        self._session.headers.update({'User-Agent': f'BFH-SSP/{bfh_science_ssp.__version__}'})
        # Follow HTTP Redirect
        self._session.allow_redirects = True
        # Add retry backoff logic
        retries = Retry(total=3, backoff_factor=0.5, status_forcelist=[502, 503, 504])
        adapter = HTTPAdapter(max_retries=retries, pool_connections=5, pool_maxsize=25)
        self._session.mount("http://", adapter)
        self._session.mount("https://", adapter)

    def stop(self):
        self.bus.log('Stop Microsoft MFA plugin')
        if self._session:
            self._session.close()
            self._session = None
        if self._conn:
            self._conn.remove_tokens_for_client()
            self._conn = None

    def _get_bearer_token(self):
        """Gets a valid token (cached or new)"""
        if not self._conn:
            raise RuntimeError('Microsoft MFA plugin is not configured')

        result = self._conn.acquire_token_for_client(scopes=["https://graph.microsoft.com/.default"])

        if 'error' in result:
            raise RuntimeError(f"Token acquisition failed: {result['error']}")

        token = result['access_token']
        return {'Authorization': f'Bearer {token}'}

    def get_authentication_methods(self, user_email):
        """
        Query user's MFA Status.
        """
        if not self._session:
            raise RuntimeWarning('Microsoft MFA plugin is not configured')

        # Equivalent to Get-MgUserAuthenticationMethod
        quoted_user_email = quote(user_email, safe="")
        url = f'https://graph.microsoft.com/v1.0/users/{quoted_user_email}/authentication/methods'
        response = self._session.get(url, headers=self._get_bearer_token())

        # Check return status
        response.raise_for_status()

        # Read data as json
        methods = response.json()

        # Extract method types
        method_types = [method['@odata.type'] for method in methods.get('value', []) if '@odata.type' in method]

        return method_types

    @classmethod
    def is_method_mfa(cls, method):
        if not method.startswith('#microsoft.graph.'):
            raise ValueError(f'invalid method: {method}')
        # FIXME to be review. I still think it's preferable to have a white list base on the docs.
        if method in MFA_TYPE:
            return False
        return True


cherrypy.microsoft_mfa = MicrosoftMfaPlugin(cherrypy.engine)
cherrypy.microsoft_mfa.subscribe()

cherrypy.config.namespaces['microsoft_mfa'] = lambda key, value: setattr(cherrypy.microsoft_mfa, key, value)
