# -*- coding: utf-8 -*-
"""
Forms to be used in the enterprise djangoapp.
"""

import re
from logging import getLogger

from edx_rbac.admin.forms import UserRoleAssignmentAdminForm
from edx_rest_api_client.exceptions import HttpClientError, HttpServerError

from django import forms
from django.conf import settings
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.db.models.fields import BLANK_CHOICE_DASH
from django.urls import reverse
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext as _

from enterprise import utils
from enterprise.admin.utils import (
    ValidationMessages,
    email_or_username__to__email,
    split_usernames_and_emails,
    validate_email_to_link,
)
from enterprise.admin.widgets import SubmitInput
from enterprise.api_client.lms import EnrollmentApiClient
from enterprise.models import (
    EnterpriseCustomer,
    EnterpriseCustomerCatalog,
    EnterpriseCustomerIdentityProvider,
    EnterpriseCustomerReportingConfiguration,
    EnterpriseFeatureUserRoleAssignment,
    SystemWideEnterpriseUserRoleAssignment,
)

try:
    from third_party_auth.models import SAMLProviderConfig as saml_provider_configuration
except ImportError:
    saml_provider_configuration = None

logger = getLogger(__name__)  # pylint: disable=invalid-name


class ManageLearnersForm(forms.Form):
    """
    Form to manage learner additions.
    """
    email_or_username = forms.CharField(
        label=_(
            "To add a single learner, enter an email address or username."),
        required=False)
    bulk_upload_csv = forms.FileField(
        label=_(
            "To add multiple learners, upload a .csv file that contains a "
            "column of email addresses."
        ),
        required=False,
        help_text=_(
            "The .csv file must have a column of email addresses, indicated "
            "by the heading 'email' in the first row."
        )
    )
    course = forms.CharField(
        label=_("Enroll these learners in this course"), required=False,
        help_text=_("To enroll learners in a course, enter a course ID."),
    )
    course_mode = forms.ChoiceField(
        label=_("Course enrollment track"), required=False,
        choices=BLANK_CHOICE_DASH + [
            ("audit", _("Audit")),
            ("verified", _("Verified")),
            ("professional", _("Professional Education")),
            ("no-id-professional", _("Professional Education (no ID)")),
            ("credit", _("Credit")),
            ("honor", _("Honor")),
        ],
    )
    reason = forms.CharField(label=_("Reason for manual enrollment"), required=False)
    sales_force_id = forms.CharField(label=_("Salesforce Opportunity ID"), required=False)
    discount = forms.DecimalField(
        label=_("Discount percentage for manual enrollment"),
        help_text=_("Discount percentage should be from 0 to 100"),
        required=True,
        decimal_places=5,
        initial=0.0
    )

    class NotificationTypes:
        """
        Namespace class for notification types
        """
        BY_EMAIL = 'by_email'
        NO_NOTIFICATION = 'do_not_notify'
        DEFAULT = getattr(settings, 'DEFAULT_ENTERPRISE_NOTIFICATION_MECHANISM', BY_EMAIL)

    notify_on_enrollment = forms.ChoiceField(
        label=_("Notify learners of enrollment"),
        choices=[
            (NotificationTypes.BY_EMAIL, _("Send email")),
            (NotificationTypes.NO_NOTIFICATION, _("Do not notify")),
        ],
        initial=NotificationTypes.DEFAULT,
        required=False,
    )

    class Modes:
        """
        Namespace class for form modes.
        """
        MODE_SINGULAR = "singular"
        MODE_BULK = "bulk"

    class Fields:
        """
        Namespace class for field names.
        """
        GENERAL_ERRORS = forms.forms.NON_FIELD_ERRORS

        EMAIL_OR_USERNAME = "email_or_username"
        BULK_UPLOAD = "bulk_upload_csv"
        MODE = "mode"
        COURSE = "course"
        COURSE_MODE = "course_mode"
        NOTIFY = "notify_on_enrollment"
        REASON = "reason"
        SALES_FORCE_ID = "sales_force_id"
        DISCOUNT = "discount"

    class CsvColumns:
        """
        Namespace class for CSV column names.
        """
        EMAIL = "email"

    def __init__(self, *args, **kwargs):
        """
        Initializes form: puts current user and enterprise_customer into a
        field for later access.

        Arguments:
            user (django.contrib.auth.models.User): current user
            enterprise_customer (enterprise.models.EnterpriseCustomer): current customer
        """
        user = kwargs.pop('user', None)
        self._user = user
        self._enterprise_customer = kwargs.pop('enterprise_customer', None)
        super(ManageLearnersForm, self).__init__(*args, **kwargs)

    def clean_email_or_username(self):
        """
        Clean email form field

        Returns:
            str: the cleaned value, converted to an email address (or an empty string)
        """
        email_or_username = self.cleaned_data[self.Fields.EMAIL_OR_USERNAME].strip()

        if not email_or_username:
            # The field is blank; we just return the existing blank value.
            return email_or_username

        email = email_or_username__to__email(email_or_username)
        bulk_entry = len(split_usernames_and_emails(email)) > 1
        if bulk_entry:
            for email in split_usernames_and_emails(email):
                validate_email_to_link(
                    email,
                    None,
                    ValidationMessages.INVALID_EMAIL_OR_USERNAME,
                    ignore_existing=True
                )
            email = email_or_username
        else:
            validate_email_to_link(
                email,
                email_or_username,
                ValidationMessages.INVALID_EMAIL_OR_USERNAME,
                ignore_existing=True
            )

        return email

    def clean_discount(self):
        """
        Verify that discount value should be from 0 to 100.
        """
        discount = self.cleaned_data[self.Fields.DISCOUNT]
        if discount < 0.0 or discount > 100.0:
            raise ValidationError(ValidationMessages.INVALID_DISCOUNT)
        return discount

    def clean_course(self):
        """
        Verify course ID and retrieve course details.
        """
        course_id = self.cleaned_data[self.Fields.COURSE].strip()
        if not course_id:
            return None
        try:
            client = EnrollmentApiClient()
            return client.get_course_details(course_id)
        except (HttpClientError, HttpServerError):
            raise ValidationError(ValidationMessages.INVALID_COURSE_ID.format(course_id=course_id))

    def clean_reason(self):
        """
        Clean the reason for enrollment field
        """
        return self.cleaned_data.get(self.Fields.REASON).strip()

    def clean_notify(self):
        """
        Clean the notify_on_enrollment field.
        """
        return self.cleaned_data.get(self.Fields.NOTIFY, self.NotificationTypes.DEFAULT)

    def clean(self):
        """
        Clean fields that depend on each other.

        In this case, the form can be used to link single user or bulk link multiple users. These are mutually
        exclusive modes, so this method checks that only one field is passed.
        """
        cleaned_data = super(ManageLearnersForm, self).clean()

        # Here we take values from `data` (and not `cleaned_data`) as we need raw values - field clean methods
        # might "invalidate" the value and set it to None, while all we care here is if it was provided at all or not
        email_or_username = self.data.get(self.Fields.EMAIL_OR_USERNAME, None)
        bulk_upload_csv = self.files.get(self.Fields.BULK_UPLOAD, None)

        if not email_or_username and not bulk_upload_csv:
            raise ValidationError(ValidationMessages.NO_FIELDS_SPECIFIED)

        if email_or_username and bulk_upload_csv:
            raise ValidationError(ValidationMessages.BOTH_FIELDS_SPECIFIED)

        if email_or_username:
            mode = self.Modes.MODE_SINGULAR
        else:
            mode = self.Modes.MODE_BULK

        cleaned_data[self.Fields.MODE] = mode
        cleaned_data[self.Fields.NOTIFY] = self.clean_notify()

        self._validate_course()
        self._validate_reason()

        return cleaned_data

    def _validate_course(self):
        """
        Verify that the selected mode is valid for the given course .
        """
        # Verify that the selected mode is valid for the given course .
        course_details = self.cleaned_data.get(self.Fields.COURSE)
        if course_details:
            course_mode = self.cleaned_data.get(self.Fields.COURSE_MODE)
            if not course_mode:
                raise ValidationError(ValidationMessages.COURSE_WITHOUT_COURSE_MODE)
            valid_course_modes = course_details["course_modes"]
            if all(course_mode != mode["slug"] for mode in valid_course_modes):
                error = ValidationError(ValidationMessages.COURSE_MODE_INVALID_FOR_COURSE.format(
                    course_mode=course_mode,
                    course_id=course_details["course_id"],
                ))
                raise ValidationError({self.Fields.COURSE_MODE: error})

    def _validate_reason(self):
        """
        Verify that the reason field is populated if the new learner(s) is being enrolled
        in a course or program.
        """
        course = self.cleaned_data.get(self.Fields.COURSE)
        reason = self.cleaned_data.get(self.Fields.REASON)

        if course and not reason:
            raise ValidationError(ValidationMessages.MISSING_REASON)


class EnterpriseCustomerAdminForm(forms.ModelForm):
    """
    Alternate form for the EnterpriseCustomer admin page.
    """
    class Meta:
        model = EnterpriseCustomer
        fields = (
            "name",
            "slug",
            "country",
            "active",
            "customer_type",
            "site",
            "enable_data_sharing_consent",
            "enforce_data_sharing_consent",
            "enable_audit_enrollment",
            "enable_audit_data_reporting",
            "replace_sensitive_sso_username",
            "hide_course_original_price",
            "enable_portal_code_management_screen",
            "enable_portal_subscription_management_screen",
            "enable_learner_portal",
            "enable_portal_reporting_config_screen",
            "enable_slug_login",
            "contact_email",
        )


class EnterpriseCustomerCatalogAdminForm(forms.ModelForm):
    """
        form for EnterpriseCustomerCatalogAdmin class.
    """
    class Meta:
        model = EnterpriseCustomerCatalog
        fields = "__all__"

    preview_button = forms.Field(required=False, label='Actions', widget=SubmitInput(attrs={'value': _('Preview')}),
                                 help_text=_("Hold Ctrl when clicking on button to open Preview in new tab"))

    @staticmethod
    def get_catalog_preview_uuid(post_data):  # pylint: disable=invalid-name
        """
        Return the uuid of the catalog the preview button was clicked on
        There must be only one preview button in the POST data.

        e.g: 'enterprise_customer_catalogs-0-preview_button'
        """
        preview_button_expression = re.compile(r'enterprise_customer_catalogs-\d+-preview_button')
        clicked_button_index_expression = re.compile(r'-(.+?)-')
        count = 0
        preview_button_index = None
        for key, _ in post_data.items():
            if preview_button_expression.match(key):
                count += 1
                preview_button_index = clicked_button_index_expression.search(key).group(1)
        if count == 1:
            return post_data.get('enterprise_customer_catalogs-' + preview_button_index + '-uuid')
        return None


class EnterpriseCustomerIdentityProviderAdminForm(forms.ModelForm):
    """
    Alternate form for the EnterpriseCustomerIdentityProvider admin page.

    This form fetches identity providers from lms third_party_auth app.
    If third_party_auth app is not avilable it displays provider_id as a CharField.
    """
    class Meta:
        model = EnterpriseCustomerIdentityProvider
        fields = "__all__"

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

        Substitutes CharField with TypedChoiceField for the provider_id field.
        """
        super(EnterpriseCustomerIdentityProviderAdminForm, self).__init__(*args, **kwargs)
        idp_choices = utils.get_idp_choices()
        help_text = ''
        if saml_provider_configuration:
            provider_id = self.instance.provider_id
            url = reverse('admin:{}_{}_add'.format(
                saml_provider_configuration._meta.app_label,
                saml_provider_configuration._meta.model_name))
            if provider_id:
                identity_provider = utils.get_identity_provider(provider_id)
                if identity_provider:
                    update_url = url + '?source={}'.format(identity_provider.pk)
                    help_text = '<p><a href="{update_url}" target="_blank">View "{identity_provider}" details</a><p>'.\
                        format(update_url=update_url, identity_provider=identity_provider.name)
                else:
                    help_text += '<p style="margin-top:-5px;"> Make sure you have added a valid provider_id.</p>'
            else:
                help_text += '<p style="margin-top:-5px;"><a target="_blank" href={add_url}>' \
                             'Create a new identity provider</a></p>'.format(add_url=url)

        if idp_choices is not None:
            self.fields['provider_id'] = forms.TypedChoiceField(
                choices=idp_choices,
                label=_('Identity Provider'),
                help_text=mark_safe(help_text),
            )

    def clean(self):
        """
        Final validations of model fields.

        1. Validate that selected site for enterprise customer matches with the selected identity provider's site.
        """
        super(EnterpriseCustomerIdentityProviderAdminForm, self).clean()

        provider_id = self.cleaned_data.get('provider_id', None)
        enterprise_customer = self.cleaned_data.get('enterprise_customer', None)

        if provider_id is None or enterprise_customer is None:
            # field validation for either provider_id or enterprise_customer has already raised
            # a validation error.
            return

        identity_provider = utils.get_identity_provider(provider_id)
        if not identity_provider:
            # This should not happen, as identity providers displayed in drop down are fetched dynamically.
            message = _(
                "The specified Identity Provider does not exist. For more "
                "information, contact a system administrator.",
            )
            # Log message for debugging
            logger.exception(message)

            raise ValidationError(message)

        if identity_provider and identity_provider.site != enterprise_customer.site:
            raise ValidationError(
                _(
                    "The site for the selected identity provider "
                    "({identity_provider_site}) does not match the site for "
                    "this enterprise customer ({enterprise_customer_site}). "
                    "To correct this problem, select a site that has a domain "
                    "of '{identity_provider_site}', or update the identity "
                    "provider to '{enterprise_customer_site}'."
                ).format(
                    enterprise_customer_site=enterprise_customer.site,
                    identity_provider_site=identity_provider.site,
                ),
            )


class EnterpriseCustomerReportingConfigAdminForm(forms.ModelForm):
    """
    Alternate form for the EnterpriseCustomerReportingConfiguration admin page.

    This form uses the PasswordInput widget to obscure passwords as they are
    being entered by the user.
    """
    enterprise_customer_catalogs = forms.ModelMultipleChoiceField(
        EnterpriseCustomerCatalog.objects.all(),
        required=False,
    )

    class Meta:
        model = EnterpriseCustomerReportingConfiguration
        fields = (
            "enterprise_customer",
            "active",
            "data_type",
            "report_type",
            "delivery_method",
            "pgp_encryption_key",
            "frequency",
            "day_of_month",
            "day_of_week",
            "hour_of_day",
            "include_date",
            "email",
            "decrypted_password",
            "sftp_hostname",
            "sftp_port",
            "sftp_username",
            "decrypted_sftp_password",
            "sftp_file_path",
            "enterprise_customer_catalogs",
        )
        widgets = {
            'decrypted_password': forms.widgets.PasswordInput(),
            'decrypted_sftp_password': forms.widgets.PasswordInput(),
        }

    def clean(self):
        """
        Override of clean method to perform additional validation
        """
        cleaned_data = super(EnterpriseCustomerReportingConfigAdminForm, self).clean()
        report_customer = cleaned_data.get('enterprise_customer')

        # Check that any selected catalogs are tied to the selected enterprise.
        invalid_catalogs = [
            '{} ({})'.format(catalog.title, catalog.uuid)
            for catalog in cleaned_data.get('enterprise_customer_catalogs')
            if catalog.enterprise_customer != report_customer
        ]

        if invalid_catalogs:
            message = _(
                'These catalogs for reporting do not match enterprise'
                'customer {enterprise_customer}: {invalid_catalogs}',
            ).format(
                enterprise_customer=report_customer,
                invalid_catalogs=invalid_catalogs,
            )
            self.add_error('enterprise_customer_catalogs', message)


class TransmitEnterpriseCoursesForm(forms.Form):
    """
    Form to transmit courses metadata for enterprise customers.
    """
    channel_worker_username = forms.CharField(
        label=_('Enter enterprise channel worker username.'),
        required=True
    )

    def clean_channel_worker_username(self):
        """
        Clean enterprise channel worker user form field

        Returns:
            str: the cleaned value of channel user username for transmitting courses metadata.
        """
        channel_worker_username = self.cleaned_data['channel_worker_username'].strip()

        try:
            User.objects.get(username=channel_worker_username)
        except User.DoesNotExist:
            raise ValidationError(
                ValidationMessages.INVALID_CHANNEL_WORKER.format(
                    channel_worker_username=channel_worker_username
                )
            )

        return channel_worker_username


class SystemWideEnterpriseUserRoleAssignmentForm(UserRoleAssignmentAdminForm):
    """
    Form for SystemWideEnterpriseUserRoleAssignments.
    """

    class Meta:
        model = SystemWideEnterpriseUserRoleAssignment
        fields = ['user', 'role']


class EnterpriseFeatureUserRoleAssignmentForm(UserRoleAssignmentAdminForm):
    """
    Form for EnterpriseFeatureUserRoleAssignments.
    """

    class Meta:
        model = EnterpriseFeatureUserRoleAssignment
        fields = ['user', 'role']