import time

import requests

from ...Logging.Logger import Severity, log
from ...Machine.PaedMLMachineCollection import LMZ_COMMON_CREDENTIALS, MACHINE_COLLECTION
from ...SetupSteps.Playbooks.Playbook import FileCleanupTask, PlaybookCredentialCollection
from ...SetupSteps.SetupStep import SetupStep


class ApplyKeycloakSettings(SetupStep):
    REALM_NAME = 'ucs'
    CLIENT_ID = 'admin-cli'
    ADMIN_USERNAME = 'Administrator'

    DISPLAY_NAME = 'paed.ML'
    SSL_REQUIRED = 'all'
    SSO_SESSION_IDLE_TIMEOUT = 3000
    SSO_SESSION_MAX_LIFESPAN = 3000
    UMC_SAML_ASSERTION_LIFESPAN = 3000

    TOKEN_RETRY_ATTEMPTS = 6
    TOKEN_RETRY_BACKOFF_SECONDS = 5

    def __init__(self, sso_fqdn: str | None = None):
        super().__init__()
        self.sso_fqdn = sso_fqdn

    def _get_access_token(self, keycloak_url: str, password: str) -> str:
        token_url = f'{keycloak_url}/realms/master/protocol/openid-connect/token'
        last_error: str | None = None
        for attempt in range(1, self.TOKEN_RETRY_ATTEMPTS + 1):
            try:
                response = requests.post(
                    token_url,
                    data={
                        'grant_type': 'password',
                        'client_id': self.CLIENT_ID,
                        'username': self.ADMIN_USERNAME,
                        'password': password,
                        'scope': 'openid',
                    },
                    headers={'Content-Type': 'application/x-www-form-urlencoded'},
                    timeout=30,
                )
            except requests.RequestException as e:
                last_error = f'connection error: {e}'
                log(f'Keycloak token request failed (attempt {attempt}/{self.TOKEN_RETRY_ATTEMPTS}): {last_error}', Severity.WARN)
            else:
                if response.status_code == 200:
                    return response.json()['access_token']

                last_error = f'HTTP {response.status_code} - {response.text}'

                # 5xx on the token endpoint is usually transient (Keycloak's LDAP federation
                # still warming up after a slapd restart). 4xx is a real auth/config problem;
                # don't waste time retrying.
                if response.status_code < 500:
                    raise RuntimeError(
                        f'Error getting access token (will not retry): {last_error}. '
                        f'Check Keycloak server log (/var/log/univention/keycloak.log) for details.'
                    )
                log(f'Keycloak token request failed (attempt {attempt}/{self.TOKEN_RETRY_ATTEMPTS}): {last_error}', Severity.WARN)

            if attempt < self.TOKEN_RETRY_ATTEMPTS:
                time.sleep(self.TOKEN_RETRY_BACKOFF_SECONDS)

        raise RuntimeError(
            f'Error getting access token after {self.TOKEN_RETRY_ATTEMPTS} attempts: {last_error}. '
            f'Check Keycloak server log (/var/log/univention/keycloak.log) for the underlying exception.'
        )

    def _get_realm_data(self, keycloak_url: str, headers: dict) -> dict:
        response = requests.get(f'{keycloak_url}/admin/realms/{self.REALM_NAME}', headers=headers)
        if response.status_code != 200:
            raise RuntimeError(f'Error fetching realm details: {response.status_code} - {response.text}')
        return response.json()

    def _put_realm_data(self, keycloak_url: str, headers: dict, realm_data: dict):
        response = requests.put(f'{keycloak_url}/admin/realms/{self.REALM_NAME}', json=realm_data, headers=headers)
        if response.status_code != 204:
            raise RuntimeError(f'Error updating realm: {response.status_code} - {response.text}')

    def _update_frontend_url(self, keycloak_url: str, headers: dict):
        realm_data = self._get_realm_data(keycloak_url, headers)
        attributes = realm_data.get('attributes', {})
        attributes['frontendUrl'] = keycloak_url
        realm_data['attributes'] = attributes
        self._put_realm_data(keycloak_url, headers, realm_data)
        log('Frontend URL updated successfully.', Severity.DEBUG)

    def _update_realm_settings(self, keycloak_url: str, headers: dict):
        realm_data = self._get_realm_data(keycloak_url, headers)
        realm_data['displayName'] = self.DISPLAY_NAME
        realm_data['sslRequired'] = self.SSL_REQUIRED
        realm_data['ssoSessionIdleTimeout'] = self.SSO_SESSION_IDLE_TIMEOUT
        realm_data['ssoSessionMaxLifespan'] = self.SSO_SESSION_MAX_LIFESPAN
        self._put_realm_data(keycloak_url, headers, realm_data)
        log('Realm settings updated successfully.', Severity.DEBUG)

    def _get_client_uuid(self, keycloak_url: str, headers: dict, client_metadata_url: str) -> str:
        response = requests.get(f'{keycloak_url}/admin/realms/{self.REALM_NAME}/clients', headers=headers)
        if response.status_code != 200:
            raise RuntimeError(f'Error fetching clients: {response.status_code} - {response.text}')
        for client in response.json():
            if client.get('clientId') == client_metadata_url:
                return client['id']
        raise RuntimeError(f'Client not found: {client_metadata_url}')

    def _update_umc_saml_client_settings(self, keycloak_url: str, headers: dict, client_metadata_urls: list[str]):
        for client_metadata_url in client_metadata_urls:
            client_uuid = self._get_client_uuid(keycloak_url, headers, client_metadata_url)
            endpoint = f'{keycloak_url}/admin/realms/{self.REALM_NAME}/clients/{client_uuid}'

            response = requests.get(endpoint, headers=headers)
            if response.status_code != 200:
                raise RuntimeError(f'Error fetching client {client_metadata_url} settings: {response.status_code} - {response.text}')

            client_settings = response.json()
            client_settings.setdefault('attributes', {})['saml.assertion.lifespan'] = self.UMC_SAML_ASSERTION_LIFESPAN

            response = requests.put(endpoint, json=client_settings, headers=headers)
            if response.status_code != 204:
                raise RuntimeError(f'Error updating client {client_metadata_url}: {response.status_code} - {response.text}')
            log(f'UMC SAML assertion lifespan updated for {client_metadata_url}.', Severity.DEBUG)

    def run(self, file_cleanup_stack: list[FileCleanupTask], credentials: PlaybookCredentialCollection) -> bool:
        log('Applying Keycloak extra settings...', Severity.INFO)

        domainname = LMZ_COMMON_CREDENTIALS.get_domainname()
        sso_fqdn = self.sso_fqdn or f'login.{domainname}'
        keycloak_url = f'https://{sso_fqdn}'

        if credentials.domadmin is None:
            log('domadmin credential is not populated; cannot authenticate against Keycloak.', Severity.ERROR)
            return False

        umc_saml_client_ids = [
            f'https://{MACHINE_COLLECTION.server.get_hostname()}/univention/saml/metadata',
            f'https://{MACHINE_COLLECTION.backup.get_hostname()}/univention/saml/metadata',
        ]

        try:
            access_token = self._get_access_token(keycloak_url, credentials.domadmin)
            headers = {
                'Authorization': f'Bearer {access_token}',
                'Content-Type': 'application/json',
            }
            self._update_frontend_url(keycloak_url, headers)
            self._update_realm_settings(keycloak_url, headers)
            self._update_umc_saml_client_settings(keycloak_url, headers, umc_saml_client_ids)
        except Exception as e:
            log(f'Failed to apply Keycloak settings: {e}', Severity.ERROR)
            return False

        log('Keycloak extra settings applied successfully.', Severity.DEBUG)
        return True
