import datetime
import logging
import json
from enum import Enum
from dataclasses import dataclass, field
from django.db.models.functions import Lower
from django.db.models import Model, QuerySet, Q, CharField, Manager
from django.utils.translation import gettext_lazy as _
from django.core.exceptions import FieldDoesNotExist
from django.core.paginator import Paginator
from accrete.contrib.ui.filter import Filter

_logger = logging.getLogger(__name__)

DEFAULT_PAGINATE_BY = 40


@dataclass
class ClientAction:

    name: str
    url: str = ''
    query_params: str = ''
    attrs: list[str] = field(default_factory=list)
    submit: bool = False
    form_id: str = 'form'
    class_list: list = field(default_factory=list)


@dataclass
class BreadCrumb:

    name: str
    url: str


class TableFieldAlignment(Enum):

    LEFT = 'left'
    CENTER = 'center'
    RIGHT = 'right'


@dataclass
class TableField:

    label: str
    name: str
    align: type[TableFieldAlignment] = TableFieldAlignment.LEFT


@dataclass
class ListContext:

    model: type[Model]
    get_params: dict
    title: str = None
    context: dict = field(default_factory=dict)
    queryset: QuerySet = None
    extra_query: Q = None
    related_fields: list[str] = field(default_factory=list)
    prefetch_fields: list[str] = field(default_factory=list)
    paginate_by: int = DEFAULT_PAGINATE_BY
    order_by: list[str] = None
    column_width: int = 12
    filter_relation_depth: int = 4
    default_filter_term: str = ''
    actions: list[ClientAction] = field(default_factory=list)
    breadcrumbs: list[BreadCrumb] = field(default_factory=list)
    obj_label: str = None
    fields: list[TableField] = field(default_factory=list)
    unselect_button: bool = False

    def get_queryset(self):
        order = self.get_order()
        if isinstance(order, str):
            if order == 'None':
                order = None
        return (self.queryset or queryset_from_querystring(
            self.model, self.get_params.get('q', '[]'), order)
                .filter(self.extra_query or Q())
                .select_related(*self.related_fields)
                .prefetch_related(*self.prefetch_fields)
                .distinct())

    def get_page_number(self, paginator):
        page_number = self.get_params.get('page', '1')

        try:
            page_number = int(page_number)
        except ValueError:
            page_number = 1

        if page_number < 1:
            page_number = 1
        elif page_number > paginator.num_pages:
            page_number = paginator.num_pages
        return page_number

    def get_order(self):
        return self.get_params.get('order_by', None) or self.order_by

    def get_paginate_by(self):
        paginate_by = self.get_params.get('paginate_by', self.paginate_by)
        try:
            paginate_by = int(paginate_by)
        except ValueError:
            paginate_by = self.paginate_by
        return paginate_by

    def get_context(self):
        paginate_by = self.get_paginate_by()
        paginator = Paginator(self.get_queryset(), paginate_by)
        page = paginator.page(self.get_page_number(paginator))
        context = {
            'paginate_by': paginate_by,
            'order_by': self.get_params.get('order_by', self.model._meta.ordering),
            'paginator': paginator,
            'page': page,
            'list_pagination': True,
            'column_width': self.column_width,
            'title': self.title or self.model._meta.verbose_name_plural,
            'filter_terms': Filter(self.model, self.filter_relation_depth).get_query_terms(),
            'default_filter_term': self.default_filter_term,
            'view_type': 'list',
            'breadcrumbs': self.breadcrumbs,
            'querystring': build_querystring(self.get_params),
            'actions': self.actions,
            'obj_label': self.obj_label or _('Name'),
            'fields': self.fields
        }
        context.update(self.context)
        return context


@dataclass
class DetailContext:

    obj: Model
    get_params: dict
    order_by: str = None
    paginate_by: int = DEFAULT_PAGINATE_BY
    title: str = None
    queryset: type[QuerySet] = None
    extra_query: Q = None
    related_fields: list[str] = field(default_factory=list)
    prefetch_fields: list[str] = field(default_factory=list)
    actions: list[ClientAction] = field(default_factory=list)
    breadcrumbs: list[BreadCrumb] = field(default_factory=list),
    context: dict = field(default_factory=dict)

    def get_queryset(self):
        order = self.get_order()
        if isinstance(order, str):
            if order == 'None':
                order = None
        return (self.queryset or queryset_from_querystring(
            self.obj._meta.model, self.get_params.get('q', '[]'), order)
                .filter(self.extra_query or Q())
                .select_related(*self.related_fields)
                .prefetch_related(*self.prefetch_fields)
                .distinct())

    def get_order(self):
        return self.get_params.get('order_by', None) or self.order_by

    def get_paginate_by(self):
        paginate_by = self.get_params.get('paginate_by', self.paginate_by)
        try:
            paginate_by = int(paginate_by)
        except ValueError:
            paginate_by = self.paginate_by
        return paginate_by

    def get_pagination_context(self):
        if not hasattr(self.obj, 'get_absolute_url'):
            _logger.warning(
                'Detail pagination disabled for models without the '
                'get_absolute_url attribute. Set paginate_by to 0 to '
                'deactivate pagination.'
            )
            return {}
        queryset = self.get_queryset()
        idx = (*queryset,).index(self.obj)
        previous_object_url = (
            queryset[idx - 1] if idx - 1 >= 0 else queryset.last()
        ).get_absolute_url()
        next_object_url = (
            queryset[idx + 1] if idx + 1 <= queryset.count() - 1 else queryset.first()
        ).get_absolute_url()
        ctx = {
            'previous_object_url': previous_object_url,
            'next_object_url': next_object_url,
            'current_object_idx': idx + 1,
            'total_objects': queryset.count(),
            'detail_pagination': True
        }
        return ctx

    def get_context(self):
        paginate_by = self.get_paginate_by()
        ctx = {
            'object': self.obj,
            'title': self.title or self.obj,
            'order_by': self.get_params.get('order_by', self.obj._meta.model._meta.ordering),
            'paginate_by': paginate_by,
            'detail_pagination': False,
            'view_type': 'detail',
            'breadcrumbs': self.breadcrumbs,
            'querystring': build_querystring(self.get_params, ['page']),
            'actions': self.actions
        }
        if self.paginate_by > 0:
            ctx.update(self.get_pagination_context())
        ctx.update(self.context)
        return ctx


@dataclass
class FormContext:

    model: Model | type[Model]
    get_params: dict
    title: str = None
    context: dict = field(default_factory=dict)
    form_id: str = 'form'
    add_default_actions: bool = True
    discard_url: str = None
    actions: list[ClientAction] = field(default_factory=list)

    def get_default_form_actions(self):
        actions = [
            ClientAction(
                name=_('Save'),
                submit=True,
                class_list=['is-success'],
                form_id=self.form_id
            )
        ]
        try:
            url = self.discard_url or self.model.get_absolute_url()
        except TypeError:
            raise TypeError(
                'Supply the discard_url parameter if FormContext is called '
                'with a model class instead of an instance.'
            )
        except AttributeError as e:
            _logger.error(
                'Supply the discard_url parameter if FormContext is '
                'called with a model instance that has the get_absolute_url '
                'method not defined.'
            )
            raise e

        actions.append(
            ClientAction(
                name=_('Discard'),
                url=url,
            )
        )
        return actions

    def get_title(self):
        if self.title:
            return self.title
        try:
            int(self.model.pk)
            return f'Edit {self.model}'
        except TypeError:
            return f'Add {self.model._meta.verbose_name}'

    def get_context(self):
        ctx = {
            'title': self.get_title(),
            'view_type': 'form',
            'form_id': self.form_id,
            'querystring': build_querystring(self.get_params, ['page']),
            'actions': []
        }
        if self.add_default_actions:
            ctx.update({'actions': self.get_default_form_actions()})
        ctx['actions'].extend(self.actions)
        ctx.update(self.context)
        return ctx


def build_querystring(get_params: dict, extra_params: list[str] = None) -> str:
    querystring = f'?q={get_params.get("q", "[]")}'
    if paginate_by := get_params.get('paginate_by', False):
        querystring += f'&paginate_by={paginate_by}'
    # if page := get_params.get('page', False):
    #     querystring += f'&page={page}'
    if order_by := get_params.get('order_by', False):
        querystring += f'&order_by={order_by}'
    for param in extra_params or []:
        if value := get_params.get(param, False):
            querystring += f'&{param}={value}'
    return querystring


def queryset_from_querystring(
        model: type[Model|Manager],
        query_string: str,
        order_by: str|list[str] = None,
) -> QuerySet:
    """
    param url_query_string: json serializable string
    [[{"name__iexact": "asdf"}, {"active": true}], [{"group__name__isnull": false}]]
    """

    if not isinstance(model, Manager):
        model = model.objects

    query_data = json.loads(query_string)
    query = Q()

    for query_block in query_data:
        block_query = Q()
        for query_term in query_block:
            if isinstance(query_term, dict):
                for param, value in query_term.items():
                    block_query &= get_query(model.model, param, value)
            elif isinstance(query_term, list):
                inner_block_query = Q()
                for inner_query_term in query_term:
                    for param, value in inner_query_term.items():
                        inner_block_query |= get_query(model.model, param, value)
                block_query &= inner_block_query
        query |= block_query

    queryset = model.filter(query)

    if order_by is None:
        return queryset

    try:
        order = order_by.split(',')
    except AttributeError:
        order = order_by or []

    for o in order_by:
        desc = False
        if o.startswith('-'):
            o = o[1:]
            desc = True
        try:
            model_field = model.model._meta.get_field(o)
        except FieldDoesNotExist:
            model_field = None
            if o == 'pk':
                pass
        if isinstance(model_field, CharField):
            if desc:
                order.append(Lower(o).desc())
            else:
                order.append(Lower(o).asc())
        else:
            order.append(f'{"-" if desc else ""}{o}')
    return queryset.order_by(*order)


def get_related_model(model, path):
    related_model = model
    for part in path:
        try:
            related_model = related_model._meta.fields_map[part].related_model
        except (AttributeError, KeyError):
            related_model = getattr(related_model, part).field.related_model
    return related_model


def get_query(model, param: str, value) -> Q:
    invert = False
    parts = param.split('__')
    if param.startswith('~'):
        param = param[1:]
        invert = True
    if len(parts) == 1:
        parts.append('exact')
    attribute = parts[-2]

    if not attribute.startswith('_c_'):
        query = Q(**{param: value})
        return ~query if invert else query

    operator = parts[-1]
    related_model = get_related_model(model, parts[:-2])
    func = getattr(related_model, attribute[3:])

    obj_ids = [
        obj.id for obj in
        filter(
            lambda instance:
            evaluate(instance, func, value, operator),
            related_model.objects.all()
        )
    ]

    query = Q(**{
        f'{"__".join(parts[:-2])}{"__" if parts[:-2] else ""}id__in': obj_ids
    })
    return ~query if invert else query


def evaluate(instance, func, value, operator):
    return_type = func.__annotations__.get('return', str)
    try:
        value = return_type(value)
    except TypeError as e:
        if isinstance(return_type, datetime.datetime):
            value = datetime.datetime.strptime(value, '%Y-%m-%dT%H:%M')
        elif isinstance(return_type, datetime.date):
            value = datetime.datetime.strptime(value, '%Y-%m-%d')
        else:
            raise e

    return_value = func(instance)

    if operator == 'exact':
        return value == return_value
    elif operator == 'icontains':
        return value.lower() in return_value.lower()
    elif operator == 'contains':
        return value in return_value
    elif operator == 'gt':
        return return_value > value
    elif operator == 'gte':
        return return_value >= value
    elif operator == 'lte':
        return return_value <= value
    elif operator == 'lt':
        return return_value < value
