from ast import literal_eval
from datetime import datetime, timedelta, date as datetime_date, time as datetime_time
from decimal import Decimal
import json
from lchttp.json import json_dumps, json_loads
import logging
from typing import Any, Sequence, TYPE_CHECKING
from uuid import UUID

from django import forms
from django.forms import (
    Field as FormField,
    NullBooleanSelect,
    TextInput,
    URLField,
    UUIDField,
)
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db.models import QuerySet
from django.utils.module_loading import import_string
from django.utils.duration import duration_iso_string
from django.utils.functional import Promise
from django.utils.safestring import SafeString
from django.utils.translation import gettext_lazy as tr

from crispy_forms.bootstrap import PrependedText
from crispy_forms.layout import (
    BaseInput,
    ButtonHolder,
    Div,
    HTML,
    Submit,
)

from .crispy_forms import Field
from .select2 import (
    LCSelect2Mixin,
    LCSelect2Widget,
)
from ..utils import tz_datetime_from_string
from ..validators import RemoteURLValidator

if TYPE_CHECKING:
    ChoiceFieldMixinBase = forms.ModelChoiceField
else:
    ChoiceFieldMixinBase = object

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


# ------------------------------------------------------------------------
class ValidatedField(Field):
    """
    Field that's automatically validated via Unpoly `up-validate`
    """

    def __init__(self, *args: Any, **kwargs: Any) -> None:
        if 'up_validate' not in kwargs:
            kwargs['up_validate'] = f'#div_id_{args[0]}'
        super().__init__(*args, **kwargs)


# ------------------------------------------------------------------------
class NameField(ValidatedField):
    def __init__(self, *args: Any, **kwargs: Any) -> None:
        defaults = {
            'autocomplete': 'off',
        }
        defaults.update(kwargs)
        super().__init__(*args, **defaults)


# ------------------------------------------------------------------------
class Reset(BaseInput):
    """
    Override class to define CSS button styling
    """

    input_type = 'reset'

    def __init__(self, *args: Any, **kwargs: Any) -> None:
        self.field_classes = 'btn btn-transition btn-outline-info'
        super().__init__(*args, **kwargs)


# ------------------------------------------------------------------------
class LCBooleanWidget(LCSelect2Mixin, forms.NullBooleanSelect):
    """Use a Select field rather than checkbox, but cannot be nullable"""

    def __init__(self, attrs: Any = None, choices: Sequence = (), **kwargs: Any) -> None:  # noqa
        super().__init__(attrs, choices)
        self.choices = (
            ('true', tr('Yes')),
            ('false', tr('No')),
        )


###################################################################################
# Predefined form fields
###################################################################################


# ------------------------------------------------------------------------
class CanonicalIDField(UUIDField):
    def __init__(self, *args: Any, **kwargs: Any) -> None:
        if 'label' not in kwargs:
            kwargs['label'] = 'Canonical ID'
        try:
            if 'help_text' not in kwargs:
                kwargs['help_text'] = field_docs.cid
        except AttributeError:
            pass
        if 'required' not in kwargs:
            kwargs['required'] = False

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


# -------------------------------------------------------------------------
class DateRangeFilterField(forms.CharField):
    def to_python(self, value: str) -> tuple[datetime, datetime] | None:  # type: ignore[override]
        """
        Set stop value to last second of day rather than calling `__date`
        on the query, which will prevent using indexes & partition
        filtering. That makes hugely inefficient queries.
        """
        if not value:
            return None

        # Single dates as being single-day ranges
        date_range = value.partition(' - ')
        time_start, time_stop = date_range[0], date_range[-1]
        if not time_stop:
            time_stop = time_start

        start = tz_datetime_from_string(time_start)
        if stop := tz_datetime_from_string(time_stop):
            stop += timedelta(days=1, seconds=-1)

        if not start or not stop:
            return None

        return start, stop


# -------------------------------------------------------------------------
class DateHashRangeFilterField(forms.CharField):
    """
    Get range values for compatibility with DateHash partitioned tables.
    """

    def to_python(self, value: str) -> tuple[str, str] | None:  # type: ignore[override]
        if not value:
            return None

        # Single dates as being single-day ranges
        date_range = value.partition(' - ')
        time_start, time_stop = date_range[0], date_range[-1]
        if not time_stop:
            time_stop = time_start

        start = tz_datetime_from_string(time_start)
        if stop := tz_datetime_from_string(time_stop):
            # Advance by one day, and use `__lt` query to get all possible values.
            stop += timedelta(days=1)

        if not start or not stop:
            return None

        return start.strftime('%Y%m%d'), stop.strftime('%Y%m%d')


# ------------------------------------------------------------------------
class LCNullBooleanWidget(LCSelect2Mixin, NullBooleanSelect):
    def __init__(self, attrs: Any = None, choices: Sequence = (), **kwargs: Any) -> None:  # noqa
        super().__init__(attrs, choices)
        self.choices = (
            ('unknown', tr('Unspecified')),
            ('true', tr('Yes')),
            ('false', tr('No')),
        )


# ----------------------------------------------------------------------
class LCNullBooleanField(forms.NullBooleanField):
    widget = LCNullBooleanWidget
    field_docs_key = ''

    def __init__(self, *args: Any, **kwargs: Any) -> None:
        if 'help_text' not in kwargs and self.field_docs_key:
            kwargs['help_text'] = getattr(field_docs, self.field_docs_key, '')
        super().__init__(*args, **kwargs)


# ----------------------------------------------------------------------
class LCBooleanSelectField(forms.NullBooleanField):
    widget = LCBooleanWidget
    field_docs_key = ''

    def __init__(self, *args: Any, **kwargs: Any) -> None:
        if 'help_text' not in kwargs and self.field_docs_key:
            kwargs['help_text'] = getattr(field_docs, self.field_docs_key, '')
        super().__init__(*args, **kwargs)


# ----------------------------------------------------------------------
class RequiredSelectField(LCBooleanSelectField):
    field_docs_key = 'required'


# ----------------------------------------------------------------------
class LCChoiceField(forms.ChoiceField):
    field_docs_key = ''

    def __init__(self, *args: Any, **kwargs: Any) -> None:
        if 'widget' not in kwargs:
            field_attrs = kwargs.pop('field_attrs', {})
            kwargs['widget'] = LCSelect2Widget(field_attrs=field_attrs)
        if 'help_text' not in kwargs and self.field_docs_key:
            kwargs['help_text'] = getattr(field_docs, self.field_docs_key, '')
        super().__init__(*args, **kwargs)


# ----------------------------------------------------------------------
class LCBooleanButtonField(LCChoiceField):
    """
    Field to use to display Checkboxes and Radio buttons
    as Bootstrap button groups.
    """

    field_docs_key = ''

    def __init__(self, *args: Any, **kwargs: Any) -> None:
        status_button = kwargs.pop('status_button', False)
        if 'widget' not in kwargs:
            kwargs['widget'] = forms.RadioSelect
        if status_button and 'label' not in kwargs:
            kwargs['label'] = tr('Record is currently')
        super().__init__(*args, **kwargs)

        if status_button:
            self.choices = (
                (True, tr('Active')),
                (False, tr('Inactive')),
            )
        else:
            self.choices = (
                (True, tr('Yes')),
                (False, tr('No')),
            )

    def to_python(self, value):
        return {
            True: True,
            'True': True,
            'False': False,
            False: False,
            'true': True,
            'false': False,
            'None': False,
            None: False,
        }.get(value)


# ----------------------------------------------------------------------
class DurationChoiceField(LCChoiceField):
    field_docs_key = ''

    def __init__(self, *args: Any, **kwargs: Any) -> None:
        label = {1: 'Hour'}
        if 'choices' not in kwargs:
            hours = kwargs.pop('hours', 12)
            choices = [(i, f'{i} {label.get(i, "Hours")}') for i in range(1, hours + 1)]
            if kwargs.pop('show_permanent', False):
                choices.append((-1, 'Permanent'))
            kwargs['choices'] = choices
        super().__init__(*args, **kwargs)

    def to_python(self, value):
        """Return an integer."""
        if value in self.empty_values:
            return 0
        return int(value)


# ----------------------------------------------------------------------
class ChoiceFieldMixin(ChoiceFieldMixinBase):
    select2_qs = QuerySet
    field_docs_key = ''

    def __init__(self, *args: Any, **kwargs: Any) -> None:
        if 'queryset' not in kwargs:
            kwargs['queryset'] = self.select2_qs

        if 'widget' not in kwargs:
            kwargs['widget'] = self.widget(queryset=kwargs['queryset'])

        if 'help_text' not in kwargs and self.field_docs_key:
            kwargs['help_text'] = getattr(field_docs, self.field_docs_key, '')

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


# ----------------------------------------------------------------------
class LCModelChoiceField(ChoiceFieldMixin, forms.ModelChoiceField):
    pass


# ----------------------------------------------------------------------
class LCModelMultipleChoiceField(ChoiceFieldMixin, forms.ModelMultipleChoiceField):
    pass


# ----------------------------------------------------------------------
def default(o):
    """
    Combined from https://bitbucket.org/schinckel/django-jsonfield
    and django.core.serializers.json.DjangoJSONEncoder
    """
    if hasattr(o, 'to_json'):
        return o.to_json()
    if isinstance(o, datetime):
        r = o.isoformat()
        if o.microsecond:
            r = r[:23] + r[26:]
        if r.endswith('+00:00'):
            r = r[:-6] + 'Z'
        return r
    if isinstance(o, datetime_date):
        return o.strftime(settings.DATE_FMT)
    if isinstance(o, datetime_time):
        if o.tzinfo:
            return o.strftime('%H:%M:%S%z')
        return o.strftime("%H:%M:%S")
    if isinstance(o, timedelta):
        return duration_iso_string(o)
    if isinstance(o, set):
        return list(o)
    if isinstance(o, (Decimal, UUID, Promise)):
        return str(o)

    raise TypeError(f"{repr(o)} is not JSON serializable")


# ----------------------------------------------------------------------
class JSONWidget(forms.Textarea):
    """
    Lifted from https://bitbucket.org/schinckel/django-jsonfield
    """

    def render(self, name, value, attrs=None, renderer=None):
        if value is None:
            value = ""
        if not isinstance(value, str):
            value = json_dumps(value, indent=2, rtype=str)
        return super().render(name, value, attrs)


# ----------------------------------------------------------------------
class JSONSelectWidget(forms.SelectMultiple):
    pass


# ----------------------------------------------------------------------
class JSONFormField(forms.CharField):
    """
    Lifted & modified from https://bitbucket.org/schinckel/django-jsonfield
    """

    empty_values = [None, '']

    def __init__(self, *args: Any, **kwargs: Any) -> None:
        if 'widget' not in kwargs:
            kwargs['widget'] = JSONWidget
        super().__init__(*args, **kwargs)

    def to_python(self, value):
        if isinstance(value, str) and value:
            try:
                return json_loads(value)
            except ValueError:
                try:
                    return literal_eval(value)
                except (SyntaxError, ValueError):
                    try:
                        return json.loads(json.dumps(value))
                    except ValueError as e:
                        raise ValidationError(f'JSON decode error: {e}') from None
        else:
            return value

    def validate(self, value):
        # This is required in older django versions.
        if value in self.empty_values and self.required:
            raise ValidationError(self.error_messages['required'], code='required')


# ----------------------------------------------------------------------

EmailFieldLayout = Field(
    PrependedText(
        'email',
        SafeString('<span class="fas fa-at"></span>'),
        placeholder=tr('Email Address'),
        autocomplete='off',
        up_validate='',
    ),
)

NextRunFieldLayout = Field(
    'next_run',
    placeholder=tr('Leave blank to auto-calculate'),
)

CanonicalIDFieldLayout = Field(
    PrependedText(
        'cid',
        SafeString(f'<span class="{settings.ICONS.CID}"></span>'),
        placeholder=tr('Leave blank to auto-calculate'),
        autocomplete='off',
    ),
)

# -------------------------------------------------------------------------
save_clear_buttons = ButtonHolder(
    Reset('reset', tr('Clear'), title='Hotkeys: Shift+Alt+X or Cmd+Shift+X'),
    Submit('submit', tr('Save'), title='Hotkeys: Shift+Alt+S or Cmd+Shift+S'),
    css_class='float-right',
)

days_of_week_selector = Div(
    Div(
        HTML('<p><span class="weekLine_days"></span></p>'),
        css_class='col-md-12',
    ),
    css_class="row",
)


# ----------------------------------------------------------------------
class IP4RangeAddressFormField(FormField):
    widget = TextInput
    default_error_messages = {
        'invalid': 'Enter a valid IP Range.',
    }

    def prepare_value(self, value):
        if isinstance(value, list):
            return '-'.join([str(ip) for ip in value])

        try:
            return value.ip
        except AttributeError:
            return value

    def validate(self, value):
        if not value and self.required:
            raise ValidationError(self.error_messages['required'], code='required')
        if value and '-' not in value:
            raise ValidationError(self.error_messages['invalid'], code='invalid')


# ----------------------------------------------------------------------
class RemoteURLField(URLField):
    """
    Ensure that URL refers to external hostname to avoid
    SSRF (Server-side Request Forgery) attacks.
    """

    default_validators = [RemoteURLValidator()]


__all__ = (
    'CanonicalIDField',
    'CanonicalIDFieldLayout',
    'DateRangeFilterField',
    'DateHashRangeFilterField',
    'DurationChoiceField',
    'EmailFieldLayout',
    'IP4RangeAddressFormField',
    'JSONFormField',
    'JSONSelectWidget',
    'JSONWidget',
    'LCBooleanButtonField',
    'LCBooleanSelectField',
    'LCBooleanWidget',
    'LCChoiceField',
    'LCModelChoiceField',
    'LCModelMultipleChoiceField',
    'LCNullBooleanField',
    'LCNullBooleanWidget',
    'NameField',
    'NextRunFieldLayout',
    'RequiredSelectField',
    'ValidatedField',
    'days_of_week_selector',
    'default',
    'field_docs',
    'Reset',
    'save_clear_buttons',
    'RemoteURLField',
)
