"""
Generic "Data Manager" — a themed, in-dashboard mirror of the Django admin.

Auto-discovers every model registered in django.contrib.admin and provides
list (with search + pagination), create, edit, and delete using Bootstrap forms.
Admin-role only. Uses the registered ModelAdmin's metadata (list_display,
search_fields, ordering) so screens stay in sync with the admin config.
"""
from django.contrib import admin, messages
from django.contrib.admin.utils import (
    display_for_field,
    display_for_value,
    label_for_field,
    lookup_field,
)
from django.core.paginator import Paginator
from django.db.models import Q
from django.forms import ModelForm, modelform_factory
from django.forms.widgets import (
    CheckboxInput,
    CheckboxSelectMultiple,
    ClearableFileInput,
    FileInput,
    RadioSelect,
    Select,
    SelectMultiple,
    Textarea,
)
from django.http import Http404
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.views import View

from utils.permissions import AdminRequiredMixin

# Models that must never be duplicated / removed from this UI.
NO_ADD = {'dashboard.sitesettings'}
NO_DELETE = {'dashboard.sitesettings'}

# Fields never exposed in the generic editor (e.g. hashed passwords).
# Set password via Manage Users → Reset password instead.
EXCLUDE_FIELDS = {'password'}

# Friendly icons per app for the Data Manager home grid.
APP_ICONS = {
    'accounts': 'bi-people',
    'courses': 'bi-collection',
    'certifications': 'bi-award',
    'enrollments': 'bi-clipboard-check',
    'mentorship': 'bi-person-video3',
    'promotions': 'bi-percent',
    'support': 'bi-life-preserver',
    'dashboard': 'bi-gear',
}


def _model_key(model):
    return f'{model._meta.app_label}.{model._meta.model_name}'


def get_registry():
    """Return {(app_label, model_name): (model, model_admin)} for registered models."""
    registry = {}
    for model, model_admin in admin.site._registry.items():
        registry[(model._meta.app_label, model._meta.model_name)] = (model, model_admin)
    return registry


def resolve(app_label, model_name):
    """Look up a registered model or raise Http404."""
    entry = get_registry().get((app_label, model_name.lower()))
    if not entry:
        raise Http404('This model is not available in the Data Manager.')
    return entry


def build_catalog():
    """Group registered models by app for the home grid."""
    groups = {}
    for (app_label, model_name), (model, model_admin) in get_registry().items():
        meta = model._meta
        try:
            count = model._default_manager.count()
        except Exception:
            count = None
        groups.setdefault(app_label, {
            'app_label': app_label,
            'app_title': app_label.replace('_', ' ').title(),
            'icon': APP_ICONS.get(app_label, 'bi-table'),
            'models': [],
        })['models'].append({
            'app_label': app_label,
            'model_name': model_name,
            'verbose_name_plural': str(meta.verbose_name_plural).title(),
            'count': count,
        })
    for group in groups.values():
        group['models'].sort(key=lambda m: m['verbose_name_plural'])
    return sorted(groups.values(), key=lambda g: g['app_title'])


def _list_display(model, model_admin):
    names = list(getattr(model_admin, 'list_display', ()) or [])
    if not names or names == ['__str__']:
        names = ['__str__']
    # Keep at most 6 columns for a clean table.
    return names[:6]


def get_headers(model, model_admin, names):
    headers = []
    for name in names:
        if name == '__str__':
            headers.append(model._meta.verbose_name.title())
            continue
        try:
            headers.append(label_for_field(name, model, model_admin))
        except Exception:
            headers.append(name.replace('_', ' ').title())
    return headers


def get_cells(obj, model_admin, names, empty='—'):
    cells = []
    for name in names:
        if name == '__str__':
            cells.append(str(obj))
            continue
        try:
            f, attr, value = lookup_field(name, obj, model_admin)
        except Exception:
            cells.append(str(getattr(obj, name, '') or empty))
            continue
        if isinstance(value, bool):
            cells.append('Yes' if value else 'No')
        elif f is None:
            cells.append(display_for_value(value, empty, False))
        else:
            try:
                cells.append(display_for_field(value, f, empty))
            except Exception:
                cells.append(str(value) if value is not None else empty)
    return cells


class BootstrapModelForm:
    """Mixin applied via modelform_factory to style widgets with Bootstrap."""

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        for field in self.fields.values():
            widget = field.widget
            if isinstance(widget, (CheckboxInput,)):
                widget.attrs.setdefault('class', 'form-check-input')
            elif isinstance(widget, (CheckboxSelectMultiple, RadioSelect)):
                widget.attrs.setdefault('class', 'form-check-input')
            elif isinstance(widget, (Select, SelectMultiple)):
                widget.attrs.setdefault('class', 'form-select')
            elif isinstance(widget, (FileInput, ClearableFileInput)):
                widget.attrs.setdefault('class', 'form-control')
            elif isinstance(widget, Textarea):
                widget.attrs.setdefault('class', 'form-control')
                widget.attrs.setdefault('rows', 4)
            else:
                widget.attrs.setdefault('class', 'form-control')


def make_form_class(model):
    base = type('BS' + model.__name__ + 'Form', (BootstrapModelForm, ModelForm), {})
    fields = [
        f.name for f in model._meta.get_fields()
        if getattr(f, 'editable', False)
        and not getattr(f, 'auto_created', False)
        and f.name not in EXCLUDE_FIELDS
    ]
    # Include editable many-to-many relations.
    for m2m in model._meta.many_to_many:
        if m2m.name not in fields and m2m.name not in EXCLUDE_FIELDS:
            fields.append(m2m.name)
    return modelform_factory(model, form=base, fields=fields)


def _can_add(key, model):
    if key in NO_ADD:
        # Singleton: allow only if none exists yet.
        try:
            return not model._default_manager.exists()
        except Exception:
            return False
    return True


def _can_delete(key):
    return key not in NO_DELETE


class DataHomeView(AdminRequiredMixin, View):
    template_name = 'dashboard/data/home.html'

    def get(self, request):
        return render(request, self.template_name, {
            'catalog': build_catalog(),
        })


class DataListView(AdminRequiredMixin, View):
    template_name = 'dashboard/data/list.html'

    def get(self, request, app_label, model_name):
        model, model_admin = resolve(app_label, model_name)
        key = _model_key(model)
        meta = model._meta

        try:
            queryset = model_admin.get_queryset(request)
        except Exception:
            queryset = model._default_manager.all()

        search_fields = list(getattr(model_admin, 'search_fields', ()) or [])
        q = (request.GET.get('q') or '').strip()
        if q and search_fields:
            cond = Q()
            for sf in search_fields:
                lookup = sf.lstrip('^=@')
                cond |= Q(**{f'{lookup}__icontains': q})
            queryset = queryset.filter(cond)

        ordering = getattr(model_admin, 'ordering', None) or meta.ordering
        if ordering:
            try:
                queryset = queryset.order_by(*ordering)
            except Exception:
                pass

        names = _list_display(model, model_admin)
        paginator = Paginator(queryset, 25)
        page = paginator.get_page(request.GET.get('page'))

        rows = []
        for obj in page.object_list:
            rows.append({
                'pk': obj.pk,
                'cells': get_cells(obj, model_admin, names),
            })

        return render(request, self.template_name, {
            'app_label': app_label,
            'model_name': model_name,
            'verbose_name': str(meta.verbose_name).title(),
            'verbose_name_plural': str(meta.verbose_name_plural).title(),
            'headers': get_headers(model, model_admin, names),
            'rows': rows,
            'page_obj': page,
            'paginator': paginator,
            'total': paginator.count,
            'q': q,
            'has_search': bool(search_fields),
            'can_add': _can_add(key, model),
            'can_delete': _can_delete(key),
        })


class DataCreateView(AdminRequiredMixin, View):
    template_name = 'dashboard/data/form.html'

    def get(self, request, app_label, model_name):
        model, _ = resolve(app_label, model_name)
        key = _model_key(model)
        if not _can_add(key, model):
            messages.error(request, 'This record cannot be created here.')
            return redirect('dashboard:data_list', app_label=app_label, model_name=model_name)
        form = make_form_class(model)()
        return render(request, self.template_name, self._ctx(model, form, 'Add'))

    def post(self, request, app_label, model_name):
        model, _ = resolve(app_label, model_name)
        key = _model_key(model)
        if not _can_add(key, model):
            raise Http404()
        form = make_form_class(model)(request.POST, request.FILES)
        if form.is_valid():
            obj = form.save()
            messages.success(request, f'{model._meta.verbose_name.title()} "{obj}" created.')
            return redirect('dashboard:data_list', app_label=app_label, model_name=model_name)
        messages.error(request, 'Please fix the errors below.')
        return render(request, self.template_name, self._ctx(model, form, 'Add'))

    @staticmethod
    def _ctx(model, form, action):
        meta = model._meta
        return {
            'app_label': meta.app_label,
            'model_name': meta.model_name,
            'verbose_name': str(meta.verbose_name).title(),
            'verbose_name_plural': str(meta.verbose_name_plural).title(),
            'form': form,
            'action': action,
            'is_edit': action == 'Edit',
        }


class DataEditView(AdminRequiredMixin, View):
    template_name = 'dashboard/data/form.html'

    def get(self, request, app_label, model_name, pk):
        model, _ = resolve(app_label, model_name)
        obj = get_object_or_404(model, pk=pk)
        form = make_form_class(model)(instance=obj)
        return render(request, self.template_name, self._ctx(model, form, obj))

    def post(self, request, app_label, model_name, pk):
        model, _ = resolve(app_label, model_name)
        obj = get_object_or_404(model, pk=pk)
        form = make_form_class(model)(request.POST, request.FILES, instance=obj)
        if form.is_valid():
            obj = form.save()
            messages.success(request, f'{model._meta.verbose_name.title()} "{obj}" updated.')
            return redirect('dashboard:data_list', app_label=app_label, model_name=model_name)
        messages.error(request, 'Please fix the errors below.')
        return render(request, self.template_name, self._ctx(model, form, obj))

    @staticmethod
    def _ctx(model, form, obj):
        meta = model._meta
        return {
            'app_label': meta.app_label,
            'model_name': meta.model_name,
            'verbose_name': str(meta.verbose_name).title(),
            'verbose_name_plural': str(meta.verbose_name_plural).title(),
            'form': form,
            'action': 'Edit',
            'is_edit': True,
            'object': obj,
            'can_delete': _can_delete(_model_key(model)),
        }


class DataDeleteView(AdminRequiredMixin, View):
    def post(self, request, app_label, model_name, pk):
        model, _ = resolve(app_label, model_name)
        key = _model_key(model)
        if not _can_delete(key):
            messages.error(request, 'This record type cannot be deleted here.')
            return redirect('dashboard:data_list', app_label=app_label, model_name=model_name)
        obj = get_object_or_404(model, pk=pk)
        label = str(obj)
        try:
            obj.delete()
            messages.success(request, f'{model._meta.verbose_name.title()} "{label}" deleted.')
        except Exception as exc:
            messages.error(request, f'Could not delete: {exc}')
        return redirect('dashboard:data_list', app_label=app_label, model_name=model_name)
