"""Administration form for authentication settings."""

import logging
from typing import Iterator, Tuple

from django import forms
from django.utils.translation import gettext_lazy as _
from djblets.forms.widgets import AmountSelectorWidget
from djblets.siteconfig.forms import SiteSettingsForm

from reviewboard.accounts.forms.auth import LegacyAuthModuleSettingsForm
from reviewboard.admin.siteconfig import load_site_config


logger = logging.getLogger(__name__)


class AuthenticationSettingsForm(SiteSettingsForm):
    """Authentication settings for Review Board.

    Attributes:
        auth_backend_forms (dict):
            A mapping of authentication backend IDs to settings form
            instances.
    """

    CUSTOM_AUTH_ID = 'custom'
    CUSTOM_AUTH_CHOICE = (CUSTOM_AUTH_ID, _('Legacy Authentication Module'))

    auth_anonymous_access = forms.BooleanField(
        label=_('Allow anonymous read-only access'),
        help_text=_('If checked, users will be able to view review requests '
                    'and diffs without logging in.'),
        required=False)

    auth_backend = forms.ChoiceField(
        label=_('Authentication Method'),
        choices=(),
        help_text=_('The method Review Board should use for authenticating '
                    'users.'),
        required=True,
        widget=forms.Select(attrs={
            'data-subform-group': 'auth-backend',
        }))

    client_web_login_enabled = forms.BooleanField(
        label=_('Enable web-based logon for clients.'),
        help_text=_('Allow users to authenticate clients (e.g. RBTools) to '
                    'Review Board by logging in via a browser.'),
        required=False)

    def __init__(self, siteconfig, *args, **kwargs):
        """Initialize the settings form.

        This will load the list of available authentication backends and
        their settings forms, allowing the browser to show the appropriate
        settings form based on the selected backend.

        Args:
            siteconfig (djblets.siteconfig.models.SiteConfiguration):
                The site configuration handling the server's settings.

            *args (tuple):
                Additional positional arguments for the parent class.

            **kwargs (dict):
                Additional keyword arguments for the parent class.
        """
        super(AuthenticationSettingsForm, self).__init__(siteconfig,
                                                         *args, **kwargs)

        from reviewboard.accounts.backends import auth_backends

        self.auth_backend_forms = {}

        cur_auth_backend = (self['auth_backend'].data or
                            self.fields['auth_backend'].initial)

        if cur_auth_backend == self.CUSTOM_AUTH_ID:
            custom_auth_form = LegacyAuthModuleSettingsForm(siteconfig,
                                                            *args, **kwargs)
        else:
            custom_auth_form = LegacyAuthModuleSettingsForm(siteconfig)

        self.auth_backend_forms[self.CUSTOM_AUTH_ID] = custom_auth_form

        backend_choices = []
        builtin_auth_choice = None

        for backend in auth_backends:
            backend_id = backend.backend_id

            try:
                if backend.settings_form:
                    if cur_auth_backend == backend_id:
                        backend_form = backend.settings_form(siteconfig,
                                                             *args, **kwargs)
                    else:
                        backend_form = backend.settings_form(siteconfig)

                    self.auth_backend_forms[backend_id] = backend_form
                    backend_form.load()

                choice = (backend_id, backend.name)

                if backend_id == 'builtin':
                    builtin_auth_choice = choice
                else:
                    backend_choices.append(choice)
            except Exception as e:
                logger.exception('Error loading authentication backend %s: %s',
                                 backend_id, e)

        backend_choices.sort(key=lambda x: x[1])
        backend_choices.insert(0, builtin_auth_choice)
        backend_choices.append(self.CUSTOM_AUTH_CHOICE)
        self.fields['auth_backend'].choices = backend_choices

        from reviewboard.accounts.sso.backends import sso_backends

        self.sso_backend_forms = {}

        form_fields = self.Meta.fieldsets[0]['fields']

        for backend in sso_backends:
            backend_id = backend.backend_id

            if backend.settings_form:
                field_id = '%s_enabled' % backend_id

                try:
                    available, reason = backend.is_available()
                except Exception as e:
                    available = False
                    reason = str(e)

                self.fields[field_id] = forms.BooleanField(
                    label=_('Enable %s Authentication') % backend.name,
                    disabled=not available,
                    help_text=reason or '',
                    required=False)

                if field_id not in form_fields:
                    form_fields.append(field_id)

                form = backend.settings_form(siteconfig, *args, **kwargs)
                form.load()
                self.sso_backend_forms[backend_id] = form

        # Settings for web-based client logon.
        client_web_login_form = ClientWebBasedLoginSettingsForm(siteconfig,
                                                                *args,
                                                                **kwargs)
        client_web_login_form.load()
        self.client_web_login_forms = {
            'client_web_login': client_web_login_form,
        }

        self.load()

    def load(self):
        """Load settings from the form.

        This will populate initial fields based on the site configuration.
        """
        super(AuthenticationSettingsForm, self).load()

        self.fields['auth_anonymous_access'].initial = \
            not self.siteconfig.get('auth_require_sitewide_login')
        self.fields['client_web_login_enabled'].initial = \
            self.siteconfig.get('client_web_login')

    def save(self):
        """Save the form.

        This will write the new configuration to the database. It will then
        force a site configuration reload.
        """
        self.siteconfig.set('auth_require_sitewide_login',
                            not self.cleaned_data['auth_anonymous_access'])
        self.siteconfig.set('client_web_login',
                            self.cleaned_data['client_web_login_enabled'])

        auth_backend = self.cleaned_data['auth_backend']

        if auth_backend in self.auth_backend_forms:
            self.auth_backend_forms[auth_backend].save()

        for form, enable_field_id in self._iter_sso_backend_forms():
            if self[enable_field_id].data:
                form.save()

        for form, enable_field_id in self._iter_client_web_login_forms():
            if self[enable_field_id].data:
                form.save()

        super(AuthenticationSettingsForm, self).save()

        # Reload any important changes into the Django settings.
        load_site_config()

    def is_valid(self):
        """Return whether the form is valid.

        This will check the validity of the fields on this form and on
        the selected authentication backend's settings form.

        Returns:
            bool:
            ``True`` if the main settings form and authentication backend's
            settings form is valid. ``False`` if either form is invalid.
        """
        if not super(AuthenticationSettingsForm, self).is_valid():
            return False

        backend_id = self.cleaned_data['auth_backend']
        backend_form = self.auth_backend_forms[backend_id]

        if not backend_form.is_valid():
            return False

        for form, enable_field_id in self._iter_sso_backend_forms():
            if (self.cleaned_data[enable_field_id] and
                not form.is_valid()):
                return False

        for form, enable_field_id in self._iter_client_web_login_forms():
            if (self.cleaned_data[enable_field_id] and
                not form.is_valid()):
                return False

        return True

    def full_clean(self):
        """Clean and validate all form fields.

        This will clean and validate both this form and the selected
        authentication backend's settings form (or all settings forms, if this
        form has not been POSTed to).

        Raises:
            django.core.exceptions.ValidationError:
                One or more fields failed validation.
        """
        super(AuthenticationSettingsForm, self).full_clean()

        if self.data:
            # Note that this isn't validated yet, but that's okay given our
            # usage. It's a bit of a hack though.
            auth_backend = (self['auth_backend'].data or
                            self.fields['auth_backend'].initial)

            if auth_backend in self.auth_backend_forms:
                self.auth_backend_forms[auth_backend].full_clean()

            for form, enable_field_id in self._iter_sso_backend_forms():
                if (self[enable_field_id].data or
                    self.fields[enable_field_id].initial):
                    form.full_clean()

            for form, enable_field_id in self._iter_client_web_login_forms():
                if (self[enable_field_id].data):
                    form.full_clean()
        else:
            for form in self.auth_backend_forms.values():
                form.full_clean()

            for form in self.sso_backend_forms.values():
                form.full_clean()

            for form in self.client_web_login_forms.values():
                form.full_clean()

    def _iter_sso_backend_forms(self):
        """Yield the SSO backend forms.

        Yields:
            tuple:
            A 2-tuple of the SSO backend form and the name of the form field
            to enable that backend.
        """
        for sso_backend_id, form in self.sso_backend_forms.items():
            enable_field_id = '%s_enabled' % sso_backend_id

            yield form, enable_field_id

    def _iter_client_web_login_forms(self) -> Iterator[Tuple[forms.Form, str]]:
        """Yield the client web-based login settings forms.

        This only yields one form.

        Yields:
            tuple:
            A 2-tuple of:

            Tuple:
                0 (django.forms.Form):
                    The client web-based login settings form.

                1 (str):
                    The name of the form field to enable the client web-based
                    login flow.
        """
        for field_id, form in self.client_web_login_forms.items():
            enable_field_id = '%s_enabled' % field_id

            yield form, enable_field_id

    class Meta:
        title = _('Authentication Settings')
        save_blacklist = ('auth_anonymous_access',)

        subforms = (
            {
                'subforms_attr': 'auth_backend_forms',
                'controller_field': 'auth_backend',
            },
            {
                'subforms_attr': 'sso_backend_forms',
                'controller_field': None,
                'enable_checkbox': True,
            },
            {
                'subforms_attr': 'client_web_login_forms',
                'controller_field': None,
                'enable_checkbox': True,
            },
        )

        fieldsets = (
            {
                'classes': ('wide',),
                'fields': ['auth_anonymous_access',
                           'auth_backend',
                           'client_web_login_enabled'],
            },
        )


class ClientWebBasedLoginSettingsForm(SiteSettingsForm):
    """A form for configuring the settings for client web-based login.

    Version Added:
        5.0.5
    """

    api_token_expiration = forms.IntegerField(
        label=_('Client API token expiration'),
        help_text=_('API tokens are automatically created to authenticate '
                    'clients during web-based login. This sets their '
                    'expiration.'),
        required=False,
        widget=AmountSelectorWidget(unit_choices=[
            (1, _('days')),
            (7, _('weeks')),
            (30, _('months')),
            (365, _('years')),
            (None, _('Never')),
        ], number_attrs={
            'min': 0,
        }))

    def load(self) -> None:
        """Load settings from the form.

        This will populate initial fields based on the site configuration.
        """
        super().load()

        self.fields['api_token_expiration'].initial = \
            self.siteconfig.get('client_web_login_token_expiration')

    def save(
        self,
        *args,
        **kwargs
    ) -> None:
        """Save the form.

        This will write the new configuration to the database. It will then
        force a site configuration reload.

        Args:
            *args (tuple):
                Positional arguments to pass to the parent method.

            **kwargs (dict):
                Keyword arguments to pass to the parent method.
        """
        self.siteconfig.set('client_web_login_token_expiration',
                            self.cleaned_data['api_token_expiration'])

        super().save(*args, **kwargs)

        # Reload any important changes into the Django settings.
        load_site_config()

    class Meta:
        title = _('Client Web-based Logon Settings')