"""
Core View Mixins for Django.

These mixins intercept standard Django HTTP rendering and interface with the
custom `UIRegistry` to construct and return Component-driven HTML trees, 
often intercepted for partial HTMX updates.
"""
from django.core.exceptions import FieldDoesNotExist, ValidationError
from django.shortcuts import redirect
from django.http import HttpResponse
from django.core.paginator import Paginator
from django.db import models
from django.template import Template, RequestContext
from lariv.registry import EnvironmentRegistry, UIRegistry
from django.views import View
from components.forms import Form, ManyToManyInput


import logging

logger = logging.getLogger(__name__)


class BaseView(View):
    """
    The root view class enabling the Python UI component model.
    Rather than rendering standard Django templates, this view orchestrates
    the building and rendering of python `Component` objects via `UIRegistry`.
    """
    def get_queryset(self):
        """Returns the base queryset for the targeted model."""
        return getattr(self, "model").objects.all()

    def get_component(self, request) -> str:
        """Returns the registered string key for the root UI component."""
        return getattr(self, "component")

    def get_key(self) -> str:
        """Returns the primary key identifier mapping data to component attributes."""
        return getattr(self, "key")

    def prepare_data(self, request, **kwargs):
        """
        Hook for injecting dynamic parameters (e.g., loaded models or pagination)
        into the component rendering context.
        """
        return {}

    def render_component(self, request, **kwargs) -> HttpResponse:
        """
        Dynamically builds the UI tree from the registry.

        If the request is via HTMX and specifies a target, it performs a surgical 
        partial render on that node. Otherwise, it renders the entire UI tree.
        """
        component_cls = UIRegistry.get(self.get_component(request))
        tree = component_cls().build()

        render_kwargs = {**kwargs, "view": self, "request": request}

        if request.htmx and request.htmx.target:
            html = tree.render_partial(request.htmx.target, **render_kwargs)
        else:
            html = tree.render(**render_kwargs)

        return HttpResponse(
            Template(html).render(
                RequestContext(request, kwargs),
            ),
        )

    def get(self, request, *args, **kwargs):
        """Standard HTTP GET handler invoking the component rendering pipeline."""
        data = self.prepare_data(request, **kwargs) or {}
        return self.render_component(request=request, **data)


class PermissionMixin:
    pass


class EnvironmentMixin:
    """
    Mixin that integrates Environment into views.
    - Filters querysets based on environment session values
    - Pre-fills forms with environment values
    - Marks filterset fields as disabled for environment fields
    """

    def get_environment_class(self):
        """Get the Environment class for the current app."""
        if hasattr(self, "environment_class"):
            return self.environment_class

        # Auto-detect from app_name
        app_name = getattr(self.request.resolver_match, "app_name", None)
        if app_name:
            try:
                return EnvironmentRegistry.get_for_app(app_name)
            except Exception:
                return None
        return None

    def get_environment(self):
        """Get an instance of the Environment for this view."""
        env_class = self.get_environment_class()
        if env_class:
            return env_class(self.request)
        return None

    def get_environment_filter_kwargs(self):
        """Get filter kwargs from environment."""
        env = self.get_environment()
        if env:
            return env.get_filter_kwargs()
        return {}

    def get_queryset(self):
        """Filter queryset based on environment values."""
        qs = super().get_queryset()
        filter_kwargs = self.get_environment_filter_kwargs()

        # Only apply filters for fields that exist on the model
        model = getattr(self, "model", None)
        if model and filter_kwargs:
            valid_kwargs = {}
            for key, value in filter_kwargs.items():
                # Check if the field exists on the model
                try:
                    model._meta.get_field(key)
                    valid_kwargs[key] = value
                except Exception:
                    pass
            if valid_kwargs:
                qs = qs.filter(**valid_kwargs)

        return qs

    def get_filterset(self, *args, **kwargs):
        """Mark environment fields as disabled in the filterset."""
        filterset = super().get_filterset(*args, **kwargs)

        if filterset is None:
            return None

        env = self.get_environment()
        if env:
            env_field_names = list(env.get_fields().keys())
            for field_name in env_field_names:
                if field_name in filterset.form.fields:
                    field = filterset.form.fields[field_name]
                    field.disabled = True
                    # Set the value from environment
                    env_values = env.get_field_values()
                    if field_name in env_values and env_values[field_name]:
                        filterset.form.initial[field_name] = env_values[field_name]

        return filterset

    def get_form(self, form_class=None):
        """Pre-fill form with environment values."""
        form = super().get_form(form_class)

        env = self.get_environment()
        if env:
            env_initial = env.get_form_initial()
            for field_name, value in env_initial.items():
                if field_name in form.fields:
                    form.fields[field_name].initial = value
                    # For create views, also set as disabled since it's from environment
                    if not getattr(self, "object", None):
                        form.fields[field_name].disabled = True

        return form

    def get_initial(self):
        """Include environment values in form initial data."""
        initial = super().get_initial()

        env = self.get_environment()
        if env:
            env_initial = env.get_form_initial()
            initial.update(env_initial)

        return initial


class LarivHtmxMixin(EnvironmentMixin):
    """
    Mixin to handle HTMX partial rendering and Environment integration.
    If the request is an HTMX request and has a target, it attempts to render
    the partial matching the target ID.
    """

    def get_template_names(self):
        templates = super().get_template_names()
        if self.request.htmx and self.request.htmx.target:
            # Append the target as the partial name
            # e.g. "batch_list.html" -> "batch_list.html#batches-list"
            return [f"{t}#{self.request.htmx.target}" for t in templates]
        return templates


def resolve_field_path(model, path):
    """
    Recursively resolves a dunder path (e.g., 'user__name') to its field object.
    """
    parts = path.split("__")
    current_model = model

    for i, part in enumerate(parts):
        field = current_model._meta.get_field(part)

        # If there are more parts, we must be looking at a relationship
        if i < len(parts) - 1:
            if not field.is_relation:
                raise FieldDoesNotExist(f"{part} is not a relationship.")
            current_model = field.related_model
        else:
            return field


def apply_filters(queryset, filters, model):
    """
    Apply filters to a queryset, handling different field types appropriately.

    - Boolean fields: exact match (True/False)
    - Foreign keys: exact match on ID
    - Text fields: icontains lookup
    - Other fields: exact match
    """
    for field_name, value in filters.items():
        if not value and value != 0 and value is not False:
            continue

        # Get the model field to determine its type
        try:
            field = resolve_field_path(model, field_name)
        except Exception:
            # Field doesn't exist on model, skip
            continue

        # Handle boolean fields
        if (
            hasattr(field, "get_internal_type")
            and field.get_internal_type() == "BooleanField"
        ):
            if value in ("True", "true", "1", True):
                queryset = queryset.filter(**{field_name: True})
            elif value in ("False", "false", "0", False):
                queryset = queryset.filter(**{field_name: False})
            # Skip if empty string or None (show all)
            continue

        # Handle foreign key fields
        if (
            hasattr(field, "get_internal_type")
            and field.get_internal_type() == "ForeignKey"
        ):
            f_keys = [int(v.strip()) for v in value.split(",")]
            try:
                queryset = queryset.filter(**{f"{field_name}__in": f_keys})
            except (ValueError, TypeError):
                pass
            continue

        # Handle text/char fields with icontains
        if hasattr(field, "get_internal_type") and field.get_internal_type() in (
            "CharField",
            "TextField",
        ):
            queryset = queryset.filter(**{f"{field_name}__icontains": value})
            continue

        # Default: exact match
        queryset = queryset.filter(**{field_name: value})

    return queryset


class ListViewMixin(LarivHtmxMixin, BaseView):
    """
    Mixin for index and list views. 
    Handles basic filtering, sorting, and pagination of QuerySets out-of-the-box.
    """
    def get_paginate_by(self, request):
        return getattr(self, "paginate_by", 12)

    def prepare_data(self, request, **kwargs):
        queryset = self.get_queryset()
        get_params = request.GET.dict()

        page_number = get_params.pop("page", 1)
        sort = get_params.pop("sort", None)
        if sort is not None:
            queryset = queryset.order_by(sort)

        queryset = apply_filters(queryset, get_params, self.model)

        paginator = Paginator(queryset, self.get_paginate_by(request))

        page = paginator.page(page_number)

        return {self.get_key(): page}


class DetailViewMixin(LarivHtmxMixin, BaseView):
    """
    Mixin for detail/read views rendering a single database record into a component.
    """
    def prepare_data(self, request, **kwargs):
        pk = kwargs["pk"]

        return {
            self.get_key(): self.get_queryset().get(id=pk),
        }


class FormViewMixin(LarivHtmxMixin, BaseView):
    """
    Mixin abstracting HTML form operations dynamically based on instantiated
    Python Input components defined in the UI tree.
    """
    def get_form_component(self):
        component_cls = UIRegistry.get(self.get_component(None))
        tree = component_cls().build()
        return self._find_form_component(tree)

    def _find_form_component(self, component):
        if isinstance(component, Form):
            if component.key == self.get_key():
                return component
        for child in component.children:
            found = self._find_form_component(child)
            if found:
                return found
        return None

    def get_inputs(self):
        form = self.get_form_component()
        if not form:
            return []
        return form._get_all_inputs(form.children)

    def get_success_url(self, obj):
        success_url = getattr(self, "success_url", None)
        if callable(success_url):
            return success_url(obj)
        return success_url

    def get_object(self, pk):
        if pk is None:
            return None
        return self.get_queryset().get(id=pk)

    def validate(self, data, inputs, instance=None):
        """
        Validate form data based on Input components.
        Returns (cleaned_data, errors) tuple.
        """
        errors = {}
        cleaned_data = {}

        for input_comp in inputs:
            field = input_comp.key
            value = data.get(field)
            # Gracefully catch fields that don't exist on the model
            try:
                model_field = self.model._meta.get_field(field) if self.model else None
            except FieldDoesNotExist:
                model_field = None

            # Check required fields for other input types
            if input_comp.required and not value:
                errors[field] = "This field is required."
            elif (
                not value
                and model_field
                and not isinstance(model_field, models.ManyToManyRel)
                and not model_field.blank
                and not model_field.null
            ):
                errors[field] = "This field is required."
            try:
                # Delegate format assertions and sanitization to the input component
                value = input_comp.clean(value)
            except ValueError as e:
                errors[field] = str(e)
                continue
            cleaned_data[field] = value

        return cleaned_data, errors

    def prepare_data(self, request, **kwargs):
        pk = kwargs.get("pk")
        obj = self.get_object(pk)

        return {
            self.get_key(): obj,
        }


class FormDataWrapper:
    """Wrapper to prioritize submitted form data over database instance data."""
    def __init__(self, instance, data):
        self._instance = instance
        self._data = data

    def __getattr__(self, name):
        if name in self._data:
            return self._data[name]
        if self._instance:
            return getattr(self._instance, name)
        raise AttributeError(name)

    @property
    def pk(self):
        return self._instance.pk if self._instance else None

    @property
    def id(self):
        return self._instance.id if self._instance else None

    def __bool__(self):
        return bool(self._instance)

    def __str__(self):
        return str(self._instance) if self._instance else ""


class PostFormViewMixin(FormViewMixin):
    def _render_form_errors(self, request, kwargs, errors, data, instance, cleaned_data=None):
        cleaned_data = cleaned_data or {}
        merged_data = {**data, **cleaned_data}
        
        prepared_data = self.prepare_data(request, **kwargs)
        prepared_data["errors"] = errors

        # Use wrapper so template receives the user-submitted data before falling back to the DB state
        prepared_data[self.get_key()] = FormDataWrapper(instance, merged_data)
        
        # Also include standard merged_data as fallback for generic UI usages
        prepared_data.update(merged_data)

        return self.render_component(
            request,
            **prepared_data,
        )

    def post(self, request, *args, **kwargs):
        pk = kwargs.get("pk")
        instance = self.get_object(pk)
        inputs = self.get_inputs()

        # Get form data from Input components
        data = {}
        m2m_data = {}  # Store ManyToMany field data separately
        for input_comp in inputs:
            field = input_comp.key
            if isinstance(input_comp, ManyToManyInput):
                # ManyToMany inputs use {key}_values for multiple hidden inputs
                values = request.POST.getlist(f"{input_comp.uid}_values")
                m2m_data[field] = values
                data[field] = values  # Also store in data for validation
            else:
                value = request.POST.get(field)
                data[field] = value

        # Validate
        cleaned_data, errors = self.validate(data, inputs, instance)

        if errors:
            return self._render_form_errors(request, kwargs, errors, data, instance, cleaned_data)

        # Separate M2M fields from regular fields for create/update
        m2m_cleaned = {}
        regular_cleaned = {}
        for field, value in cleaned_data.items():
            if field in m2m_data:
                m2m_cleaned[field] = value
            else:
                regular_cleaned[field] = value

        # Create or update
        try:
            if instance:
                for field, value in regular_cleaned.items():
                    setattr(instance, field, value)
                instance.save()
                obj = instance
            else:
                obj = self.model.objects.create(**regular_cleaned)

            # Set ManyToMany fields after object is saved
            for field, values in m2m_cleaned.items():
                getattr(obj, field).set(values)

        # If an error occurs, return the form with the error
        except ValidationError as e:
            if hasattr(e, "message_dict"):
                for field, msg_list in e.message_dict.items():
                    errors[field] = ", ".join(msg_list)
            else:
                errors["Error"] = str(e)

            return self._render_form_errors(request, kwargs, errors, data, instance, cleaned_data)
        except Exception as e:
            errors["Error"] = str(e)

            return self._render_form_errors(request, kwargs, errors, data, instance, cleaned_data)

        # Redirect to success URL
        success_url = self.get_success_url(obj)
        if request.htmx:
            return HttpResponse(
                status=200,
                headers={"HX-Redirect": success_url},
            )

        return redirect(success_url)


class DeleteViewMixin(LarivHtmxMixin, BaseView):
    """
    Mixin for handling dynamic database record deletion operations, gracefully
    handling HTMX redirects on success.
    """

    def get_object(self, pk):
        return self.get_queryset().get(id=pk)

    def get_success_url(self):
        return getattr(self, "success_url", "/")

    def prepare_data(self, request, **kwargs):
        pk = kwargs["pk"]
        return {
            self.get_key(): self.get_object(pk),
        }

    def post(self, request, *args, **kwargs):
        pk = kwargs["pk"]
        obj = self.get_object(pk)
        obj.delete()

        success_url = self.get_success_url()
        if request.htmx:
            return HttpResponse(
                status=200,
                headers={"HX-Redirect": success_url},
            )
        from django.shortcuts import redirect

        return redirect(success_url)


class SelectionTableViewMixin(BaseView):
    """
    Mixin for serving selection tables in modals for foreign key and many-to-many inputs.

    Attributes:
        model: The Django model to select from
        component: The table component name (should use Table with select="single" or select="multi")
        key: Key to pass the queryset to the component
        paginate_by: Number of items per page (default 6)
    """

    paginate_by = 6

    def get_paginate_by(self, request):
        return getattr(self, "paginate_by", 6)

    def prepare_data(self, request, **kwargs):
        queryset = self.get_queryset()
        get_params = request.GET.dict()

        # Extract pagination and modal params
        page_number = get_params.pop("page", 1)
        target_input = get_params.pop("target_input", "")
        modal_id = get_params.pop("modal_id", "")

        # Apply filters from remaining params
        queryset = apply_filters(queryset, get_params, self.model)

        paginator = Paginator(queryset, self.get_paginate_by(request))
        page = paginator.page(page_number)

        return {
            self.get_key(): page,
            "target_input": target_input,
            "modal_id": modal_id,
        }


class ChartViewMixin(BaseView):
    """
    A view mixin designed to work seamlessly with the Chart component.
    It returns a JsonResponse of chart data if the request accepts application/json.
    Otherwise, it falls back to rendering the standard BaseView HTML component logic.
    """

    def get_chart_data(self, request, **kwargs):
        """
        Override this method to return the dataset for the chart.
        Must return a dict compatible with ApexCharts options.
        e.g., {'series': [{'name': 'Data', 'data': [1,2,3]}], 'xaxis': {'categories': ['A','B','C']}}
        """
        return {}

    def get(self, request, *args, **kwargs):
        # Check if the request explicitly asks for JSON (e.g., from the Chart JS fetch)
        accept_header = request.headers.get("Accept", "")
        if "application/json" in accept_header:
            from django.http import JsonResponse

            data = self.get_chart_data(request, **kwargs)
            return JsonResponse(data)

        # Fallback to the standard HTML component rendering
        return super().get(request, *args, **kwargs)
