import gzip
import json
from collections import OrderedDict, defaultdict
from pathlib import Path

from django.apps import apps
from django.conf import settings
from django.contrib.admin.models import LogEntry
from django.contrib.auth import get_user_model
from django.contrib.sessions.models import Session
from django.core.management import call_command
from django.db import transaction
from django.db.models import FileField
from django.utils import timezone
from django_otp.plugins.otp_totp.models import TOTPDevice

from .models import DatabaseCleanupOperation
from .registry import CLEANUP_GROUPS, FULL_RESET_GROUPS, GROUP_DEPENDENCIES, MODEL_DELETE_ORDER


class CleanupError(Exception):
    pass


def maintenance_enabled():
    return bool(getattr(settings, "K4W_DATABASE_MAINTENANCE_ENABLED", False))


def full_reset_enabled():
    return bool(getattr(settings, "K4W_DATABASE_MAINTENANCE_ALLOW_FULL_RESET", False))


def expand_groups(selected_groups):
    expanded = set(selected_groups)
    changed = True
    while changed:
        changed = False
        for group in tuple(expanded):
            for dependency in GROUP_DEPENDENCIES.get(group, set()):
                if dependency not in expanded:
                    expanded.add(dependency)
                    changed = True
    return [key for key in CLEANUP_GROUPS if key in expanded]


def _model_from_label(label):
    app_label, model_name = label.split(".", 1)
    return apps.get_model(app_label, model_name)


def _models_for_group(group_key):
    definition = CLEANUP_GROUPS[group_key]
    models = []
    for label in definition.get("models", []):
        model = _model_from_label(label)
        if model:
            models.append(model)
    for app_label in definition.get("apps", []):
        try:
            config = apps.get_app_config(app_label)
        except LookupError:
            continue
        models.extend(list(config.get_models()))
    unique = OrderedDict()
    for model in models:
        unique[model._meta.label] = model
    return list(unique.values())


def _client_user_ids():
    Access = apps.get_model("clients", "ClientPortalAccess")
    return list(Access.objects.values_list("user_id", flat=True))


def _special_count(group_key, current_user):
    User = get_user_model()
    if group_key == "client_accounts":
        ids = _client_user_ids()
        return User.objects.filter(pk__in=ids).exclude(pk=current_user.pk).count()
    if group_key == "security_client_data":
        ids = _client_user_ids()
        Profile = apps.get_model("security", "UserProfile")
        RecoveryCode = apps.get_model("security", "RecoveryCode")
        SecurityEvent = apps.get_model("security", "SecurityEvent")
        return (
            Profile.objects.filter(user_id__in=ids).exclude(user_id=current_user.pk).count()
            + RecoveryCode.objects.filter(user_id__in=ids).exclude(user_id=current_user.pk).count()
            + SecurityEvent.objects.filter(user_id__in=ids).exclude(user_id=current_user.pk).count()
            + TOTPDevice.objects.filter(user_id__in=ids).exclude(user_id=current_user.pk).count()
        )
    if group_key == "other_staff_accounts":
        return User.objects.filter(is_staff=True).exclude(pk=current_user.pk).count()
    if group_key == "sessions_logs":
        current_session = getattr(getattr(current_user, "_request", None), "session", None)
        session_count = Session.objects.count()
        if current_session and current_session.session_key:
            session_count = Session.objects.exclude(session_key=current_session.session_key).count()
        return session_count + LogEntry.objects.count()
    return 0


def count_group(group_key, current_user):
    definition = CLEANUP_GROUPS[group_key]
    if definition.get("special"):
        return _special_count(group_key, current_user)
    return sum(model._default_manager.count() for model in _models_for_group(group_key))


def _format_model_label(model):
    return str(model._meta.verbose_name_plural).title()


def _count_model_files(model):
    fields = [field for field in model._meta.fields if isinstance(field, FileField)]
    if not fields:
        return 0, 0
    count = 0
    size = 0
    values = model._default_manager.values_list(*[field.name for field in fields])
    for row in values.iterator():
        for index, name in enumerate(row):
            if not name:
                continue
            count += 1
            try:
                size += fields[index].storage.size(name)
            except Exception:
                pass
    return count, size


def preview_cleanup(selected_groups, current_user, options=None):
    options = options or {}
    selected = [key for key in CLEANUP_GROUPS if key in set(selected_groups)]
    effective = expand_groups(selected)

    if options.get("preserve_configuration") and "portal_configuration" in effective:
        effective.remove("portal_configuration")
    if options.get("preserve_templates"):
        effective = [key for key in effective if key not in {"template_library", "email_templates"}]
    if options.get("preserve_automation") and "automation" in effective:
        effective.remove("automation")
    if options.get("preserve_other_staff") and "other_staff_accounts" in effective:
        effective.remove("other_staff_accounts")

    groups = []
    model_totals = defaultdict(int)
    total_records = 0
    file_count = 0
    file_size = 0
    seen_models = set()

    for key in effective:
        definition = CLEANUP_GROUPS[key]
        if definition.get("special"):
            count = _special_count(key, current_user)
            groups.append({"key": key, "label": definition["label"], "count": count, "auto": key not in selected})
            total_records += count
            continue

        group_count = 0
        for model in _models_for_group(key):
            if model._meta.label in seen_models:
                continue
            seen_models.add(model._meta.label)
            count = model._default_manager.count()
            if count:
                model_totals[_format_model_label(model)] += count
            group_count += count
            total_records += count
            if options.get("delete_files"):
                fc, fs = _count_model_files(model)
                file_count += fc
                file_size += fs
        groups.append({"key": key, "label": definition["label"], "count": group_count, "auto": key not in selected})

    return {
        "selected_groups": selected,
        "effective_groups": effective,
        "auto_groups": [key for key in effective if key not in selected],
        "groups": groups,
        "models": [{"label": label, "count": count} for label, count in sorted(model_totals.items())],
        "total_records": total_records,
        "file_count": file_count,
        "file_size": file_size,
        "current_admin_preserved": current_user.get_username(),
    }


def create_backup_snapshot(operation):
    backup_dir = Path(settings.BASE_DIR) / "backups" / "database"
    backup_dir.mkdir(parents=True, exist_ok=True)
    timestamp = timezone.now().strftime("%Y%m%d-%H%M%S")
    path = backup_dir / f"k4w-before-cleanup-{timestamp}-{str(operation.code)[:8]}.json.gz"
    with gzip.open(path, "wt", encoding="utf-8") as output:
        call_command(
            "dumpdata",
            exclude=[
                "contenttypes",
                "auth.permission",
                "sessions",
                "admin.logentry",
                "maintenance",
            ],
            natural_foreign=True,
            natural_primary=True,
            indent=2,
            stdout=output,
            verbosity=0,
        )
    return str(path.relative_to(settings.BASE_DIR))


def _ordered_models(effective_groups):
    selected_models = OrderedDict()
    for key in effective_groups:
        definition = CLEANUP_GROUPS[key]
        if definition.get("special"):
            continue
        for model in _models_for_group(key):
            selected_models[model._meta.label] = model

    ordered = []
    for label in MODEL_DELETE_ORDER:
        model = selected_models.pop(label, None)
        if model:
            ordered.append(model)

    # Primeiro os modelos normais restantes e, no fim, tabelas históricas.
    remaining = list(selected_models.values())
    remaining.sort(key=lambda model: (model.__name__.startswith("Historical"), model._meta.label))
    ordered.extend(remaining)
    return ordered


def _collect_files(models):
    refs = []
    for model in models:
        file_fields = [field for field in model._meta.fields if isinstance(field, FileField)]
        if not file_fields:
            continue
        for field in file_fields:
            for name in model._default_manager.exclude(**{field.name: ""}).values_list(field.name, flat=True).iterator():
                if name:
                    refs.append((field.storage, name))
    return refs


def _delete_special(group_key, current_user, captured_client_user_ids, current_session_key):
    User = get_user_model()
    deleted = 0
    detail = {}
    if group_key == "security_client_data":
        ids = [pk for pk in captured_client_user_ids if pk != current_user.pk]
        Profile = apps.get_model("security", "UserProfile")
        RecoveryCode = apps.get_model("security", "RecoveryCode")
        SecurityEvent = apps.get_model("security", "SecurityEvent")
        for label, queryset in (
            ("Perfis de utilizadores", Profile.objects.filter(user_id__in=ids)),
            ("Códigos de recuperação", RecoveryCode.objects.filter(user_id__in=ids)),
            ("Eventos de segurança", SecurityEvent.objects.filter(user_id__in=ids)),
            ("Dispositivos TOTP", TOTPDevice.objects.filter(user_id__in=ids)),
        ):
            count, _ = queryset.delete()
            detail[label] = count
            deleted += count
    elif group_key == "client_accounts":
        queryset = User.objects.filter(pk__in=captured_client_user_ids).exclude(pk=current_user.pk)
        count, breakdown = queryset.delete()
        detail.update(breakdown)
        deleted += count
    elif group_key == "other_staff_accounts":
        queryset = User.objects.filter(is_staff=True).exclude(pk=current_user.pk)
        count, breakdown = queryset.delete()
        detail.update(breakdown)
        deleted += count
    elif group_key == "sessions_logs":
        sessions = Session.objects.all()
        if current_session_key:
            sessions = sessions.exclude(session_key=current_session_key)
        count, breakdown = sessions.delete()
        deleted += count
        detail.update(breakdown)
        count, breakdown = LogEntry.objects.all().delete()
        deleted += count
        for label, value in breakdown.items():
            detail[label] = detail.get(label, 0) + value
    return deleted, detail


def execute_cleanup(operation, current_user, current_session_key=""):
    if operation.status != DatabaseCleanupOperation.Status.PREVIEWED:
        raise CleanupError("Esta operação já não está disponível para execução.")
    if not current_user.is_superuser:
        raise CleanupError("Apenas um superutilizador pode executar esta operação.")

    operation.status = DatabaseCleanupOperation.Status.RUNNING
    operation.started_at = timezone.now()
    operation.save(update_fields=["status", "started_at"])

    file_refs = []
    deleted_breakdown = defaultdict(int)
    total_deleted = 0
    captured_client_user_ids = _client_user_ids()

    try:
        if operation.options.get("create_backup", True):
            operation.backup_path = create_backup_snapshot(operation)
            operation.save(update_fields=["backup_path"])

        models = _ordered_models(operation.effective_groups)
        if operation.options.get("delete_files"):
            file_refs = _collect_files(models)
            if "security_client_data" in operation.effective_groups:
                Profile = apps.get_model("security", "UserProfile")
                avatar_field = Profile._meta.get_field("avatar")
                for name in Profile.objects.filter(user_id__in=captured_client_user_ids).exclude(user_id=current_user.pk).exclude(avatar="").values_list("avatar", flat=True):
                    if name:
                        file_refs.append((avatar_field.storage, name))

        with transaction.atomic():
            for model in models:
                count, breakdown = model._default_manager.all().delete()
                total_deleted += count
                for label, value in breakdown.items():
                    deleted_breakdown[label] += value

            # Segurança deve ser removida antes das contas, mantendo sempre o administrador atual.
            for special in ("security_client_data", "client_accounts", "other_staff_accounts", "sessions_logs"):
                if special not in operation.effective_groups:
                    continue
                count, breakdown = _delete_special(
                    special,
                    current_user=current_user,
                    captured_client_user_ids=captured_client_user_ids,
                    current_session_key=current_session_key,
                )
                total_deleted += count
                for label, value in breakdown.items():
                    deleted_breakdown[label] += value

        deleted_files = 0
        file_errors = []
        if operation.options.get("delete_files"):
            for storage, name in file_refs:
                try:
                    if storage.exists(name):
                        storage.delete(name)
                        deleted_files += 1
                except Exception as exc:
                    file_errors.append(f"{name}: {exc}")

        operation.status = DatabaseCleanupOperation.Status.COMPLETED
        operation.completed_at = timezone.now()
        operation.result = {
            "total_deleted": total_deleted,
            "breakdown": dict(sorted(deleted_breakdown.items())),
            "deleted_files": deleted_files,
            "file_errors": file_errors,
            "preserved_admin": current_user.get_username(),
        }
        operation.save(update_fields=["status", "completed_at", "result"])
        return operation.result
    except Exception as exc:
        operation.status = DatabaseCleanupOperation.Status.FAILED
        operation.completed_at = timezone.now()
        operation.error_message = str(exc)
        operation.save(update_fields=["status", "completed_at", "error_message"])
        raise


def build_group_sections(current_user):
    sections = OrderedDict()
    for key, definition in CLEANUP_GROUPS.items():
        section = definition["section"]
        sections.setdefault(section, [])
        sections[section].append(
            {
                "key": key,
                "label": definition["label"],
                "description": definition["description"],
                "icon": definition["icon"],
                "tone": definition["tone"],
                "count": count_group(key, current_user),
            }
        )
    return [{"title": title, "groups": groups} for title, groups in sections.items()]
