import logging
from sequential_uuids.generators import uuid_time_nextval
from typing import Any, Union
from unpoly.forms import UnpolyCrispyFormMixin

from django import forms
from django.conf import settings
from django.core.exceptions import NON_FIELD_ERRORS, ValidationError
from django.forms.widgets import Media
from django.db.models import Func
from django.utils.module_loading import import_string
from django.utils.translation import gettext_lazy as tr

from ..utils.storage import get_current_user

logger = logging.getLogger(__name__)
try:
    field_docs = import_string(settings.FIELD_DOCS_PATH)
except ImportError:
    field_docs = object

CREATING = 'creating'
UPDATING = 'updating'
COMPANY = 'company'
POLICY = 'policy'
NAME = 'name'
CODE = 'code'
CID = 'cid'
TENANT_FIELDS = (COMPANY, POLICY)
NAME_FIELDS = (NAME, CODE)


# ----------------------------------------------------------------------
class LCFormSetupBase(UnpolyCrispyFormMixin):
    def __init__(self, *args: Any, **kwargs: Any) -> None:
        """
        Setup form for Console use...

        1. Pop user from form kwargs
        2. Setup company cache
        3. Add aria-required attribute to form fields
        """
        # Is this record universally applying? Neither Company nor Policy assigned?
        self.cloning_existing_record = kwargs.pop(settings.CLONING_RECORD_KEY, False)
        self.is_universal = kwargs.pop(settings.UNIVERSAL_RECORD_KEY, False)
        self.user = kwargs.pop("user", get_current_user())
        self.form_tag = kwargs.pop('form_tag', True)

        super().__init__(*args, **kwargs)
        self.helper.include_media = False
        self.helper.form_tag = self.form_tag
        self._set_aria_attrs()
        self._record_is_universal = self.record_is_universal()

        self.check_tenant_fields()

    def record_is_universal(self) -> bool:
        """
        Record is universal if kwarg specified thus when creating,
        or if record has neither Company nor Policy tenant.
        """
        if self.is_creating():
            return self.is_universal

        try:
            return self.instance.is_universal()
        except AttributeError:
            pass
        return False

    def crud_op(self) -> str:
        """
        Helper method to determine whether an object is being updated or created
        """
        # Standard Form won't have instance attribute, so consider
        # all form activity by forms.Form to be "creating"
        if not hasattr(self, 'instance'):
            return CREATING

        # This mostly won't work on models with UUID PK
        # there it'll always be present as UUID or database function
        if not getattr(self.instance, 'pk', None) or isinstance(self.instance.pk, Func):
            return CREATING

        if hasattr(self.instance, 'get_dirty_fields') and 'id' in self.instance.get_dirty_fields():
            return CREATING

        # won't work unless modified field is "auto_now", and it usually won't be
        if not getattr(self.instance, 'modified', None):
            return CREATING

        return UPDATING

    def is_creating(self) -> bool:
        return self.crud_op() == CREATING

    def is_updating(self) -> bool:
        return self.crud_op() == UPDATING

    def check_tenant_fields(self) -> None:
        """
        Hide Company / Policy tenant fields if the record is universal
        """
        if self.cloning_existing_record or not self._record_is_universal:
            return

        for tenant in settings.TENANT_FIELDS:
            if tenant in self.fields:
                self.fields[tenant].widget = forms.HiddenInput()
                self.fields[tenant].initial = None

    def check_prefill(self, field: str, possible_values: Union[list, tuple]) -> None:
        """
        If Select field has only one possible value available to the user,
        set the field's initial value and hide the field, to save steps for the user.
        """
        # Only prefill if creating a new record.
        if self.is_updating():
            return

        # don't prefill policy field, as some records won't _require_ it
        # and with the permissions ramifications, it should be deliberately set.
        if field == POLICY:
            return

        if self._record_is_universal:
            return

        if field in self.fields and len(possible_values) < 2:
            if self.fields[field].required:
                self.fields[field].widget = forms.HiddenInput()
            if len(possible_values) == 1:
                self.fields[field].initial = possible_values[0]
                self.initial[field] = possible_values[0]

    def _set_aria_attrs(self) -> None:
        """
        Set Aria attributes on form fields
        """
        for bound_field in self:
            if not hasattr(bound_field, "field"):
                continue
            if bound_field.field.required:
                bound_field.field.widget.attrs["aria-required"] = "true"
            else:
                bound_field.field.widget.attrs["aria-required"] = "false"

    def display_form_errors(self) -> None:
        """
        Call in `form.clean` method, after added errors to the form. Call multiple times
        if it's critical to clean up errors in stages, as validation checks may be
        performed in stages.
        """
        if self.errors:
            logger.info('Form %s errors: %s', self.__class__.__name__, self.errors)

            for field in self.hidden_fields():
                if not self.errors:
                    continue
                for error in field.errors:
                    self.add_error(NON_FIELD_ERRORS, f'{field.label} - {error}')

            raise ValidationError('Form has errors!')


# ----------------------------------------------------------------------
class LCModelFormMixin(LCFormSetupBase):
    def __init__(self, *args: Any, **kwargs: Any) -> None:
        super().__init__(*args, **kwargs)

        # Don't require a CID on creation so we can
        # attempt to generate a deterministic CID
        if CID in self.fields and self.is_creating():
            self.fields[CID].required = False

        # Some forms will have tenant fields included
        # only for select field filtering purposes.
        # Then don't validate tenants.
        self.tenant_fields_filter_only = False

    def model_reserved_names(self) -> set[str]:
        try:
            return self._meta.model.reserved_names  # noqa
        except AttributeError:
            return set()

    def validate_reserved_names(self, data: dict) -> None:
        """
        Reserved names may not be set or changed via forms.
        """
        reserved_names = self.model_reserved_names()

        if not reserved_names:
            return

        # no worry about names changing if the fields aren't in the form!
        fields = set(self._meta.fields)  # noqa
        if NAME not in fields and CODE not in fields:
            return

        if self.is_creating():
            for field in NAME_FIELDS:
                value = data.get(field, '')
                if value and value.lower() in reserved_names:
                    error = ValidationError(tr('Reserved value. Please try something else.'))
                    self.add_error(field, error)

        if self.is_updating():
            for field in NAME_FIELDS:
                old_value = getattr(self.instance, field, '').lower()
                new_value = data.get(field, '').lower()

                # Handle empty values by default form validation
                if not new_value:
                    continue

                if old_value in reserved_names and new_value not in reserved_names:
                    error = ValidationError(
                        tr('Original value is reserved. Cannot change this field.')
                    )
                    self.add_error(field, error)

    def validate_tenant_fields(self, data: dict) -> None:
        """
        Company / Policy are the tenant fields on console records.

        Most records will REQUIRE one or both fields to be specified.

        Universal records REQUIRE neither field to be specified.
        """
        if self.tenant_fields_filter_only or self.cloning_existing_record:
            return

        record_is_universal = self.record_is_universal()

        if not record_is_universal:
            if not data[POLICY] and not data[COMPANY]:
                msg = tr('Please specify a %(tenant)s')
                for tenant in TENANT_FIELDS:
                    self.add_error(tenant, ValidationError(msg % {'tenant': tr(tenant).title()}))

        else:
            msg = tr('Cannot assign Universal record to a %(tenant)s')
            for tenant in TENANT_FIELDS:
                if data.get(tenant, None):
                    self.add_error(tenant, ValidationError(msg % {'tenant': tr(tenant).title()}))

    def set_tenant_fields(self, data: dict) -> None:
        """
        Set tenant fields on instance so that "dirty_fields" values are updated.
        Must be updated so that calling the instance's `clean` method has the
        data needed to validate correctly.
        """
        for field in TENANT_FIELDS:
            try:
                setattr(self.instance, field, data[field])
            except (AttributeError, KeyError):
                # one of the tenant columns is in the form
                # data but not a field on the model.
                continue

    def check_universal_record_perms(self) -> None:
        if not self.is_creating():
            return

        if self.record_is_universal():
            if not self.user.is_reseller:
                error = ValidationError(
                    tr('You do not have required permissions to create universal records')
                )
                self.add_error(NON_FIELD_ERRORS, error)
            if not settings.SYNC_PUBLISHER:
                error = ValidationError(
                    tr('Universal records may only be created on Sync Publishers')
                )
                self.add_error(NON_FIELD_ERRORS, error)

    def clean(self) -> dict:
        """
        On all forms that contain either Company or Policy,
        ensure that at least one field has a value, unless
        the model attribute overrides.
        """
        data = super().clean()
        self.validate_reserved_names(data)

        self.display_form_errors()

        if COMPANY not in data or POLICY not in data:
            return data

        self.validate_tenant_fields(data=data)
        self.set_tenant_fields(data=data)
        self.check_universal_record_perms()

        self.display_form_errors()

        return data

    def save(self, commit: bool = True) -> Any:
        """
        Save user-supplied CID or default on objects created via forms
        """
        creating = self.is_creating()
        changed_fields = self.changed_data
        has_cid_field = 'cid' in self.Meta.fields

        obj = super().save(commit=False)

        if creating and has_cid_field and 'cid' not in changed_fields:
            obj.cid = uuid_time_nextval()

        if commit:
            obj.save()
            self.save_m2m()

        return obj


# ----------------------------------------------------------------------
class LCFormBase(LCFormSetupBase, forms.Form):
    @property
    def media(self):
        """
        Form doesn't need to handle any assets.
        """
        return Media()


# ----------------------------------------------------------------------
class LCModelFormBase(LCModelFormMixin, forms.ModelForm):
    pass


__all__ = (
    'LCFormBase',
    'LCFormSetupBase',
    'LCModelFormBase',
    'LCModelFormMixin',
)
