import django_filters
from django import forms
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Div, Field
from collections import OrderedDict

from .models import (
    Case,
    CaseCollection,
    Condition,
    Finding,
    PathologicalProcess,
    Presentation,
    Procedure,
    QuestionSchema,
    Series,
    Structure,
    Subspecialty,
    NormalCase,
    Resource,
    Site,
)
from django.contrib.auth.models import User
from django.db.models import Q
from django.http import QueryDict


def get_authors(request):
    if request is None:
        return User.objects.none()

    if not request.user.groups.filter(name="atlas_editor").exists():
        queryset = User.objects.filter(id=request.user.id)
    else:
        queryset = User.objects.all()

    return queryset


def get_cases(request):
    if request is None:
        return Case.objects.none()

    if not request.user.groups.filter(name="atlas_editor").exists():
        queryset = Case.objects.filter(author__id=request.user.id)
    else:
        queryset = Case.objects.all()

    return queryset


def get_series(request):
    if request is None:
        return Series.objects.none()

    if not request.user.groups.filter(name="atlas_editor").exists():
        queryset = Series.objects.filter(author__id=request.user.id)
    else:
        queryset = Series.objects.all()

    queryset = queryset.prefetch_related("examination", "plane")

    return queryset


class CaseCollectionFilter(django_filters.FilterSet):
    author = django_filters.ModelMultipleChoiceFilter(
        queryset=get_authors, null_label="No author"
    )
    cases = django_filters.ModelMultipleChoiceFilter(
        queryset=get_cases, null_label="No cases"
    )

    class Meta:
        model = CaseCollection
        fields = (
            "archive",
            "author",
        )  # "site",

    def __init__(
        self,
        data=None,
        queryset=None,
        prefix=None,
        strict=None,
        user=None,
        request=None,
    ):
        # Default the archive filter to False when not provided so collections
        # are not archived by default in listings.
        if data is None:
            data = QueryDict('', mutable=True)
            data['archive'] = 'false'
        else:
            # QueryDict instances need to be copied to be mutable
            try:
                if 'archive' not in data:
                    data = data.copy()
                    data['archive'] = 'false'
            except Exception:
                # Fallback for non-QueryDict mappings
                if isinstance(data, dict) and 'archive' not in data:
                    data = dict(data)
                    data['archive'] = 'false'
        if not request.user.groups.filter(name="atlas_editor").exists():
            queryset = queryset.prefetch_related("author").filter(
                author__id=request.user.id
            )
        super(CaseCollectionFilter, self).__init__(
            data=data, queryset=queryset, prefix=prefix, request=request
        )
        pass


class CaseFilter(django_filters.FilterSet):
    author = django_filters.ModelMultipleChoiceFilter(
        queryset=get_authors, null_label="No author"
    )
    # Show a distinct list of `Series.examination` values instead of raw Series
    # entries so examinations that differ only by plane/description appear once.
    series = django_filters.ModelMultipleChoiceFilter(
        field_name='series__examination',
        queryset=lambda request: __import__('generic.models', fromlist=['Examination']).Examination.objects.filter(atlas_series_examination__isnull=False).distinct(),
        label='Examination',
    )
    # Free-text search to help find the correct examination when there are
    # many similarly-named Series/Examination entries.
    examination_text = django_filters.CharFilter(method='filter_examination_text', label='Examination (text)')
    # Full list of conditions and a text search to find conditions
    condition = django_filters.ModelMultipleChoiceFilter(
        queryset=Condition.objects.all(),
        label='Condition',
    )
    condition_text = django_filters.CharFilter(method='filter_condition_text', label='Condition (text)')
    differential_case = django_filters.ModelMultipleChoiceFilter(
        label="Differential",
        field_name="differentialcase__condition",
        # to_field_name="differential",
        queryset=Condition.objects.all(),
    )

    class Meta:
        model = Case
        fields = (
            "created_date",
            "open_access",
            "author",
            "differential_case",
        )  # "site",

    def __init__(
        self,
        data=None,
        queryset=None,
        prefix=None,
        strict=None,
        user=None,
        request=None,
    ):
        if not request.user.groups.filter(name="atlas_editor").exists():
            queryset = queryset.prefetch_related("author").filter(
                (Q(author__id=request.user.id) | Q(open_access=True))
            )
        super(CaseFilter, self).__init__(
            data=data, queryset=queryset, prefix=prefix, request=request
        )
        pass

    def filter_examination_text(self, queryset, name, value):
        """Filter Cases by a text search on related Examination records.

        Matches against the Examination string representation and any
        common identifying fields (name/code). Uses a case-insensitive
        contains lookup and filters Cases which have a Series linked to
        a matching Examination.
        """
        if not value:
            return queryset

        from generic.models import Examination

        # Only attempt to match PK when the provided value looks like an integer
        try:
            pk_val = int(value)
        except Exception:
            pk_val = None

        if pk_val is not None:
            matches = Examination.objects.filter(
                Q(examination__icontains=value) | Q(pk=pk_val)
            ).distinct()
        else:
            matches = Examination.objects.filter(
                Q(examination__icontains=value)
            ).distinct()

        return queryset.filter(series__examination__in=matches).distinct()

    def filter_condition_text(self, queryset, name, value):
        """Filter Cases by a text search on related Condition records.

        Matches both direct `condition` M2M and `differential` entries.
        """
        if not value:
            return queryset

        # Only try PK match when numeric
        try:
            pk_val = int(value)
        except Exception:
            pk_val = None

        if pk_val is not None:
            matches = Condition.objects.filter(Q(name__icontains=value) | Q(pk=pk_val)).distinct()
        else:
            matches = Condition.objects.filter(Q(name__icontains=value)).distinct()

        # Match either direct condition M2M or differential (through Differential)
        return queryset.filter(Q(condition__in=matches) | Q(differential__in=matches)).distinct()


class SeriesFilter(django_filters.FilterSet):
    case = django_filters.ModelMultipleChoiceFilter(
        queryset=get_cases, null_label="No case"
    )
    author = django_filters.ModelMultipleChoiceFilter(
        queryset=get_authors, null_label="No author"
    )

    class Meta:
        model = Series
        fields = ("modality", "case", "examination", "author")

    def __init__(
        self,
        data=None,
        queryset=None,
        prefix=None,
        strict=None,
        user=None,
        request=None,
    ):
        if not request.user.groups.filter(name="atlas_editor").exists():
            # queryset = queryset.filter(open_access=True) | queryset.filter(author__id=request.user.id)
            queryset = queryset.filter(author__id=request.user.id)
        super(SeriesFilter, self).__init__(
            data=data, queryset=queryset, prefix=prefix, request=request
        )
        pass


class ConditionFilter(django_filters.FilterSet):
    # Replace legacy `primary` filter with `is_canonical` and provide a
    # `synonym` filter which will filter by the canonical group of the
    # selected condition.
    is_canonical = django_filters.BooleanFilter(method="filter_is_canonical", label="Canonical")
    synonym = django_filters.ModelChoiceFilter(queryset=Condition.objects.all(), method="filter_synonym", label="Synonym group")

    class Meta:
        model = Condition
        fields = {
            "name": ["icontains"],
            "subspecialty": ["exact"],
            "rcr_curriculum": ["exact"],
        }

    def __init__(
        self,
        data=None,
        queryset=None,
        prefix=None,
        strict=None,
        user=None,
        request=None,
    ):
        super(ConditionFilter, self).__init__(
            data=data, queryset=queryset, prefix=prefix, request=request
        )
        pass

    def filter_is_canonical(self, queryset, name, value):
        if value in (True, "True", "true", 1, "1"):
            return queryset.filter(canonical__isnull=True)
        if value in (False, "False", "false", 0, "0"):
            return queryset.filter(canonical__isnull=False)
        return queryset

    def filter_synonym(self, queryset, name, value):
        # value is a Condition instance selected in the filter. Return all
        # Conditions that belong to the same canonical group as `value`.
        if not value:
            return queryset
        master = value.canonical if value.canonical else value
        return queryset.filter(Q(canonical=master) | Q(pk=master.pk))


class FindingFilter(django_filters.FilterSet):
    # Replace legacy `primary`/`synonym` with canonical-based filters
    is_canonical = django_filters.BooleanFilter(method="filter_is_canonical", label="Canonical")
    synonym = django_filters.ModelChoiceFilter(queryset=Finding.objects.all(), method="filter_synonym", label="Synonym group")

    class Meta:
        model = Finding
        fields = {
            "name": ["icontains"],
        }

    def __init__(
        self,
        data=None,
        queryset=None,
        prefix=None,
        strict=None,
        user=None,
        request=None,
    ):
        super(FindingFilter, self).__init__(
            data=data, queryset=queryset, prefix=prefix, request=request
        )
        pass

    def filter_is_canonical(self, queryset, name, value):
        if value in (True, "True", "true", 1, "1"):
            return queryset.filter(canonical__isnull=True)
        if value in (False, "False", "false", 0, "0"):
            return queryset.filter(canonical__isnull=False)
        return queryset

    def filter_synonym(self, queryset, name, value):
        # value is a Finding instance selected in the filter. Return all
        # Findings that belong to the same canonical group as `value`.
        if not value:
            return queryset
        master = value.canonical if value.canonical else value
        return queryset.filter(Q(canonical=master) | Q(pk=master.pk))


class StructureFilter(django_filters.FilterSet):
    is_canonical = django_filters.BooleanFilter(method="filter_is_canonical", label="Canonical")
    synonym = django_filters.ModelChoiceFilter(queryset=Structure.objects.all(), method="filter_synonym", label="Synonym group")

    class Meta:
        model = Structure
        fields = {"name": ["icontains"]}

    def __init__(
        self,
        data=None,
        queryset=None,
        prefix=None,
        strict=None,
        user=None,
        request=None,
    ):
        super(StructureFilter, self).__init__(
            data=data, queryset=queryset, prefix=prefix, request=request
        )
        pass

    def filter_is_canonical(self, queryset, name, value):
        if value in (True, "True", "true", 1, "1"):
            return queryset.filter(canonical__isnull=True)
        if value in (False, "False", "false", 0, "0"):
            return queryset.filter(canonical__isnull=False)
        return queryset

    def filter_synonym(self, queryset, name, value):
        if not value:
            return queryset
        master = value.canonical if value.canonical else value
        return queryset.filter(Q(canonical=master) | Q(pk=master.pk))


class PresentationFilter(django_filters.FilterSet):
    class Meta:
        model = Presentation
        fields = ("name", "subspecialty")

    def __init__(
        self,
        data=None,
        queryset=None,
        prefix=None,
        strict=None,
        user=None,
        request=None,
    ):
        super(PresentationFilter, self).__init__(
            data=data, queryset=queryset, prefix=prefix, request=request
        )
        pass


class PathologicalProcessFilter(django_filters.FilterSet):
    class Meta:
        model = PathologicalProcess
        fields = ("name",)

    def __init__(
        self,
        data=None,
        queryset=None,
        prefix=None,
        strict=None,
        user=None,
        request=None,
    ):
        super(PathologicalProcessFilter, self).__init__(
            data=data, queryset=queryset, prefix=prefix, request=request
        )
        pass


class SubspecialtyFilter(django_filters.FilterSet):
    class Meta:
        model = Subspecialty
        fields = ("name",)

    def __init__(
        self,
        data=None,
        queryset=None,
        prefix=None,
        strict=None,
        user=None,
        request=None,
    ):
        super(SubspecialtyFilter, self).__init__(
            data=data, queryset=queryset, prefix=prefix, request=request
        )
        pass


class NormalCaseFilter(django_filters.FilterSet):
    """FilterSet for the NormalCase model allowing filtering by
    examination, modality and nearest-year age ranges.
    """
    examination = django_filters.ModelChoiceFilter(queryset=Series.objects.none())
    modality = django_filters.ModelChoiceFilter(queryset=Series.objects.none())
    procedures = django_filters.ModelMultipleChoiceFilter(field_name="case__procedures", queryset=Procedure.objects.all(), label="Procedure")
    # Accept min/max as years in the UI but filter against the canonical
    # `age_days` field by converting years -> days.
    # Numeric min/max values entered by the user (interpreted via `age_unit`)
    min_value = django_filters.NumberFilter(label="Min", method="filter_min_value")
    max_value = django_filters.NumberFilter(label="Max", method="filter_max_value")
    AGE_UNIT_CHOICES = (
        ("days", "Days"),
        ("weeks", "Weeks"),
        ("months", "Months"),
        ("years", "Years"),
    )
    age_unit = django_filters.ChoiceFilter(label="Unit", choices=AGE_UNIT_CHOICES, method="filter_age_unit")

    class Meta:
        model = NormalCase
        fields = ("examination", "modality", "procedures", "age_days")

    def __init__(self, data=None, queryset=None, prefix=None, strict=None, user=None, request=None):
        # Use the global Examination and Modality querysets from the Series model
        from generic.models import Examination, Modality

        # We'll set the actual querysets for the filters after applying
        # any NormalCase queryset restrictions for the current user so
        # that the choices only include examinations/modalities that
        # actually have NormalCase records available.

        # If not an atlas editor restrict to cases authored by the user or open access cases
        if request is not None and not request.user.groups.filter(name="atlas_editor").exists():
            from django.db.models import Q

            if queryset is None:
                queryset = NormalCase.objects.select_related("case")
            queryset = queryset.filter((Q(case__author__id=request.user.id) | Q(case__open_access=True)))

        # Determine the NormalCase queryset we'll use to build choices
        nc_qs = queryset if queryset is not None else NormalCase.objects.all()

        # Limit examination and modality choices to those present on the
        # available NormalCase records (respecting user restrictions).
        try:
            exam_ids = nc_qs.values_list("examination_id", flat=True)
            mod_ids = nc_qs.values_list("modality_id", flat=True)
            proc_ids = nc_qs.values_list("case__procedures", flat=True)
            self.base_filters.get("examination").queryset = Examination.objects.filter(id__in=exam_ids).distinct()
            self.base_filters.get("modality").queryset = Modality.objects.filter(id__in=mod_ids).distinct()
            self.base_filters.get("procedures").queryset = Procedure.objects.filter(id__in=proc_ids).distinct()
        except Exception:
            # Fallback to all if something goes wrong
            self.base_filters.get("examination").queryset = Examination.objects.all()
            self.base_filters.get("modality").queryset = Modality.objects.all()

        super(NormalCaseFilter, self).__init__(data=data, queryset=queryset, prefix=prefix, request=request)

        # Attach a Crispy FormHelper to render the filter form as a
        # horizontal Bootstrap grid. We set `form_tag=False` because the
        # template already provides the <form> element.
        try:
            helper = FormHelper()
            helper.form_method = "get"
            helper.form_tag = False
            helper.form_class = "row g-2"
            helper.layout = Layout(
                Div(
                    Div("examination", css_class="col-md-3"),
                    Div("modality", css_class="col-md-3"),
                    Div("procedures", css_class="col-md-3"),
                    css_class="row g-2",
                ),
                Div(
                    Div(
                        Field("min_value", wrapper_class="mb-0 d-flex align-items-center"),
                        Field("max_value", wrapper_class="mb-0 d-flex align-items-center"),
                        Field("age_unit", wrapper_class="mb-0 d-flex align-items-center"),
                        css_class="col-12 d-flex gap-2 align-items-center",
                    ),
                    css_class="row g-2",
                ),
            )
            # Assign helper to the generated form instance and tweak widgets
            if hasattr(self, 'form') and self.form is not None:
                # Wider numeric inputs and default unit
                try:
                    form = self.form
                    if "min_value" in form.fields:
                        form.fields["min_value"].widget = forms.NumberInput(attrs={"class": "form-control w-auto", "min": "0", "step": "1", "style": "min-width:140px;"})
                    if "max_value" in form.fields:
                        form.fields["max_value"].widget = forms.NumberInput(attrs={"class": "form-control w-auto", "min": "0", "step": "1", "style": "min-width:140px;"})
                    if "age_unit" in form.fields:
                        form.fields["age_unit"].initial = "days"
                        form.fields["age_unit"].widget.attrs.update({"class": "form-select w-auto", "style": "min-width:220px;"})
                    form.helper = helper
                except Exception:
                    # don't crash if form structure differs
                    self.form.helper = helper
        except Exception:
            pass

    def _convert_to_days(self, value, unit: str) -> int:
        try:
            v = float(value)
        except Exception:
            raise

        if unit == "days":
            return int(v)
        if unit == "weeks":
            return int(v * 7)
        if unit == "months":
            # approximate month length
            return int(v * (365.25 / 12.0))
        # default: years
        return int(v * 365.25)

    def filter_min_value(self, queryset, name, value):
        try:
            unit = getattr(self.form, "cleaned_data", {}).get("age_unit", "days")
        except Exception:
            unit = "days"
        try:
            days = self._convert_to_days(value, unit)
        except Exception:
            return queryset
        return queryset.filter(age_days__gte=days)

    def filter_max_value(self, queryset, name, value):
        try:
            unit = getattr(self.form, "cleaned_data", {}).get("age_unit", "days")
        except Exception:
            unit = "days"
        try:
            days = self._convert_to_days(value, unit)
        except Exception:
            return queryset
        return queryset.filter(age_days__lte=days)

    def filter_age_unit(self, queryset, name, value):
        # `age_unit` is a helper input used by min/max conversion and does not
        # map to a database field on NormalCase.
        return queryset


class ResourceFilter(django_filters.FilterSet):
    name = django_filters.CharFilter(field_name='name', lookup_expr='icontains')
    site = django_filters.ModelMultipleChoiceFilter(field_name='sites', queryset=lambda request: Site.objects.all() if True else Site.objects.none())
    subspecialty = django_filters.ModelMultipleChoiceFilter(field_name='subspecialty', queryset=Subspecialty.objects.all())
    author = django_filters.ModelMultipleChoiceFilter(queryset=get_authors, null_label='No author')
    has_file = django_filters.BooleanFilter(method='filter_has_file', label='Has file')

    class Meta:
        model = Resource
        fields = ('name', 'site', 'subspecialty', 'author')

    def filter_has_file(self, queryset, name, value):
        if value is True:
            return queryset.exclude(file__isnull=True).exclude(file__exact='')
        elif value is False:
            return queryset.filter(file__isnull=True) | queryset.filter(file__exact='')
        return queryset

class QuestionSchemaFilter(django_filters.FilterSet):
    class Meta:
        model = QuestionSchema
        fields = ("name", "description")