from django import forms
from astropy.coordinates import Angle
from astropy import units as u
from django.forms import ValidationError, inlineformset_factory
from django.conf import settings
from django.contrib.auth.models import Group
from guardian.shortcuts import assign_perm, get_groups_with_perms, remove_perm

from tom_dataproducts.sharing import get_sharing_destination_options
from .models import Target, TargetExtra, TargetName, TargetList, PersistentShare
from tom_targets.base_models import (SIDEREAL_FIELDS, NON_SIDEREAL_FIELDS, REQUIRED_SIDEREAL_FIELDS,
                                     REQUIRED_NON_SIDEREAL_FIELDS, REQUIRED_NON_SIDEREAL_FIELDS_PER_SCHEME,
                                     IGNORE_FIELDS)


def extra_field_to_form_field(field_type):
    if field_type == 'number':
        return forms.FloatField(required=False)
    elif field_type == 'boolean':
        return forms.BooleanField(required=False)
    elif field_type == 'datetime':
        return forms.DateTimeField(required=False)
    elif field_type == 'string':
        return forms.CharField(required=False, widget=forms.Textarea)
    else:
        raise ValueError(
            'Invalid field type {}. Field type must be one of: number, boolean, datetime string'.format(field_type)
        )


class CoordinateField(forms.CharField):
    def __init__(self, *args, **kwargs):
        c_type = kwargs.pop('c_type')
        self.c_type = c_type
        super().__init__(*args, **kwargs)

    def to_python(self, value):
        try:
            a = float(value)
            return a
        except ValueError:
            try:
                if self.c_type == 'ra':
                    a = Angle(value, unit=u.hourangle)
                else:
                    a = Angle(value, unit=u.degree)
                return a.to(u.degree).value
            except Exception:
                raise ValidationError('Invalid format. Please use sexigesimal or degrees')


class TargetForm(forms.ModelForm):
    groups = forms.ModelMultipleChoiceField(Group.objects.none(), required=False, widget=forms.CheckboxSelectMultiple)

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.extra_fields = {}
        for extra_field in settings.EXTRA_FIELDS:
            # Add extra fields to the form
            field_name = extra_field['name']
            self.extra_fields[field_name] = extra_field_to_form_field(extra_field['type'])
            # Populate them with initial values if this is an update
            if kwargs['instance']:
                te = TargetExtra.objects.filter(target=kwargs['instance'], key=field_name)
                if te.exists():
                    self.extra_fields[field_name].initial = te.first().typed_value(extra_field['type'])

            self.fields.update(self.extra_fields)

    def save(self, commit=True):
        instance = super().save(commit=commit)
        if commit:
            for field in settings.EXTRA_FIELDS:
                if self.cleaned_data.get(field['name']) is not None:
                    updated_target_extra, _ = TargetExtra.objects.update_or_create(
                        target=instance,
                        key=field['name'],
                        defaults={'value': self.cleaned_data[field['name']]}
                    )
                    updated_target_extra.save()
            # Save groups for this target
            for group in self.cleaned_data['groups']:
                assign_perm('tom_targets.view_target', group, instance)
                assign_perm('tom_targets.change_target', group, instance)
                assign_perm('tom_targets.delete_target', group, instance)
            for group in get_groups_with_perms(instance):
                if group not in self.cleaned_data['groups']:
                    remove_perm('tom_targets.view_target', group, instance)
                    remove_perm('tom_targets.change_target', group, instance)
                    remove_perm('tom_targets.delete_target', group, instance)

        return instance

    class Meta:
        abstract = True
        model = Target
        fields = '__all__'
        widgets = {'type': forms.HiddenInput()}


class SiderealTargetCreateForm(TargetForm):
    ra = CoordinateField(required=True, label='Right Ascension', c_type='ra',
                         help_text='Right Ascension, in decimal degrees or sexagesimal hours. See '
                                   'https://docs.astropy.org/en/stable/api/astropy.coordinates.Angle.html for '
                                   'supported sexagesimal inputs.')
    dec = CoordinateField(required=True, label='Declination', c_type='dec',
                          help_text='Declination, in decimal or sexagesimal degrees. See '
                                    ' https://docs.astropy.org/en/stable/api/astropy.coordinates.Angle.html for '
                                    'supported sexagesimal inputs.')

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        for field in REQUIRED_SIDEREAL_FIELDS:
            self.fields[field].required = True

    class Meta(TargetForm.Meta):
        # Include Sidereal Fields and User defined fields that are not included in the Base Target model.
        fields = SIDEREAL_FIELDS + [field.name for field in Target._meta.get_fields()
                                    if field not in Target._meta.related_objects and
                                    field.name not in SIDEREAL_FIELDS + IGNORE_FIELDS + NON_SIDEREAL_FIELDS]


class NonSiderealTargetCreateForm(TargetForm):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        for field in REQUIRED_NON_SIDEREAL_FIELDS:
            self.fields[field].required = True

    def clean(self):
        """
        Look at the 'scheme' field and check the fields required for the
        specified field have been given
        """
        cleaned_data = super().clean()
        scheme = cleaned_data['scheme']  # scheme is a required field, so this should be safe
        required_fields = REQUIRED_NON_SIDEREAL_FIELDS_PER_SCHEME[scheme]

        for field in required_fields:
            if not cleaned_data.get(field):
                # Get verbose names of required fields
                field_names = [
                    "'" + Target._meta.get_field(f).verbose_name + "'"
                    for f in required_fields
                ]
                scheme_name = dict(Target.TARGET_SCHEMES)[scheme]
                raise ValidationError(
                    "Scheme '{}' requires fields {}".format(scheme_name, ', '.join(field_names))
                )

    class Meta(TargetForm.Meta):
        # Include Non-Sidereal Fields and User defined fields that are not included in the Base Target model.
        fields = NON_SIDEREAL_FIELDS + [field.name for field in Target._meta.get_fields()
                                        if field not in Target._meta.related_objects and
                                        field.name not in SIDEREAL_FIELDS + IGNORE_FIELDS + NON_SIDEREAL_FIELDS]


class UnknownTypeTargetCreateForm(TargetForm):
    """If we don't know the type, this provides a generic Target Creation form that requires type to be set.
    The only difference between this and the base TargetForm is that the 'type' field is required, and not hidden.
    """
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['type'].required = True

    class Meta(TargetForm.Meta):
        widgets = {}


class TargetVisibilityForm(forms.Form):
    start_time = forms.DateTimeField(required=True, label='Start Time', widget=forms.TextInput(attrs={'type': 'date'}))
    end_time = forms.DateTimeField(required=True, label='End Time', widget=forms.TextInput(attrs={'type': 'date'}))
    airmass = forms.DecimalField(required=False, label='Maximum Airmass')

    def clean(self):
        cleaned_data = super().clean()
        start_time = cleaned_data.get('start_time')
        end_time = cleaned_data.get('end_time')
        target = self.data['target']
        if end_time < start_time:
            raise forms.ValidationError('Start time must be before end time')
        if target.type == 'NON_SIDEREAL':
            raise forms.ValidationError('Airmass plotting is only supported for sidereal targets')


TargetExtraFormset = inlineformset_factory(Target, TargetExtra, fields=('key', 'value'),
                                           widgets={'value': forms.TextInput()})
TargetNamesFormset = inlineformset_factory(Target, TargetName, fields=('name',), validate_min=False, can_delete=True,
                                           extra=3)


class TargetShareForm(forms.Form):
    """
    Form for sharing a target with an outside destination such as another TOM Toolkit or Hermes
    """
    share_destination = forms.ChoiceField(required=True, choices=[], label="Destination")
    share_title = forms.CharField(required=False, label="Title")
    share_message = forms.CharField(required=False, label="Message", widget=forms.Textarea(attrs={'rows': 4}))
    target = forms.ModelChoiceField(
        Target.objects.all(),
        widget=forms.HiddenInput(),
        required=True)
    submitter = forms.CharField(widget=forms.HiddenInput())

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['share_destination'].choices = get_sharing_destination_options()


class TargetListShareForm(forms.Form):
    """
    Form for sharing a target list with an outside destination such as another TOM Toolkit or Hermes
    """
    share_destination = forms.ChoiceField(required=True, choices=[], label="Destination")
    share_title = forms.CharField(required=False, label="Title")
    share_message = forms.CharField(required=False, label="Message", widget=forms.Textarea(attrs={'rows': 4}))
    target_list = forms.ModelChoiceField(
        TargetList.objects.all(),
        widget=forms.HiddenInput(),
        required=True)
    submitter = forms.CharField(widget=forms.HiddenInput())

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['share_destination'].choices = get_sharing_destination_options()


class TargetMergeForm(forms.Form):
    """
    Form for merging two duplicate targets with a primary target and secondary target
    """
    name_select = forms.ChoiceField(
        label="Select Primary Target",
        required=True,
        choices=[],
        # Select is the default widget for a ChoiceField, but we need to set htmx attributes.
        widget=forms.Select(
            # set up attributes to trigger folder dropdown update when this field changes
            attrs={
                'hx-get': '',  # send GET request to the source URL's get method
                'hx-trigger': 'change',  # when this happens
                'hx-target': '#id_target_merge_fields',  # replace name_select element
             })
    )


class AdminPersistentShareForm(forms.ModelForm):
    destination = forms.ChoiceField(choices=[], label='Share Destination', required=True)

    class Meta:
        model = PersistentShare
        fields = '__all__'

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['destination'].choices = get_sharing_destination_options(include_download=False)


class PersistentShareForm(AdminPersistentShareForm):
    target = forms.IntegerField(label='Target ID', initial=0, required=True)
    share_existing_data = forms.BooleanField(label='Share existing data immediately', required=False, initial=False)

    def __init__(self, *args, **kwargs):
        try:
            self.target_id = kwargs.pop('target_id')
        except KeyError:
            self.target_id = None
        super().__init__(*args, **kwargs)
        if self.target_id:
            self.fields['target'].initial = self.target_id
            self.fields['target'].disabled = True
