"""Administración de cuentas, roles e invitaciones."""

import hashlib
import secrets
from datetime import timedelta
from functools import wraps
from zoneinfo import ZoneInfo

from django.conf import settings
from django.contrib.auth import login as django_login
from django.contrib.auth.decorators import login_required
from django.contrib.auth.password_validation import validate_password
from django.core.exceptions import ValidationError
from django.core.mail import EmailMultiAlternatives
from django.core.validators import validate_email
from django.db import transaction
from django.db.models import Case, CharField, Q, Value, When
from django.http import HttpResponseForbidden, JsonResponse
from django.shortcuts import get_object_or_404, redirect
from django.template.loader import render_to_string
from django.urls import reverse
from django.utils import timezone
from django.views.decorators.http import require_http_methods
from inertia import render

from apps.accounts.models import Account, AccountRole, Invitation
from apps.accounts.services import close_sessions
from apps.core.requests import field
from apps.core.tables import Filter, TableSpec, table_props

USERS = TableSpec(
    sortable={
        'email': ('email',),
        'role': ('role', 'email'),
        'status': ('management_status', 'email'),
        'created': ('created_at',),
    },
    default_sort='email',
    filters=(
        Filter(param='query', lookup='email__icontains'),
        Filter(param='role', lookup='role', choices=(AccountRole.ADMIN, AccountRole.VIEWER)),
        Filter(
            param='status', lookup='management_status', choices=('ACTIVE', 'INVITED', 'INACTIVE')
        ),
    ),
)


def admin_required(view):
    """Restringe la gestión de usuarios a los dos roles administrativos."""

    @login_required
    @wraps(view)
    def wrapped(request, *args, **kwargs):
        if request.user.role not in (AccountRole.SUPER_ADMIN, AccountRole.ADMIN):
            return HttpResponseForbidden()
        return view(request, *args, **kwargs)

    return wrapped


def _accounts():
    return Account.objects.exclude(role=AccountRole.SUPER_ADMIN).annotate(
        management_status=Case(
            When(is_active=True, then=Value('ACTIVE')),
            When(
                Q(invitation__isnull=False) & Q(invitation__accepted_at=None), then=Value('INVITED')
            ),
            default=Value('INACTIVE'),
            output_field=CharField(),
        )
    )


def _row(account: Account) -> dict:
    return {
        'id': str(account.id),
        'email': account.email,
        'role': account.role,
        'is_active': account.is_active,
        'status': account.management_status,
        'created_at': account.created_at.isoformat(),
    }


@admin_required
def index(request):
    return render(
        request, 'Users', props={'users': table_props(request, _accounts(), USERS, serialize=_row)}
    )


def _token_hash(token: str) -> str:
    return hashlib.sha256(token.encode()).hexdigest()


@admin_required
@require_http_methods(['POST'])
@transaction.atomic
def invite(request):
    email = field(request, 'email').strip().lower()
    role = field(request, 'role')
    try:
        validate_email(email)
    except ValidationError:
        return JsonResponse({'error': {'code': 'EMAIL_INVALID', 'field': 'email'}}, status=422)
    if role not in (AccountRole.ADMIN, AccountRole.VIEWER):
        return JsonResponse({'error': {'code': 'ROLE_INVALID', 'field': 'role'}}, status=422)
    if Account.objects.filter(email__iexact=email).exists():
        return JsonResponse({'error': {'code': 'EMAIL_TAKEN', 'field': 'email'}}, status=409)
    account = Account.objects.create_user(email=email, role=role, is_active=False)
    account.set_unusable_password()
    account.is_staff = role == AccountRole.ADMIN
    account.save(update_fields=['password', 'is_staff', 'updated_at'])
    token = secrets.token_urlsafe(32)
    expires_at = timezone.now() + timedelta(days=3)
    Invitation.objects.create(
        account=account,
        invited_by=request.user,
        token_hash=_token_hash(token),
        expires_at=expires_at,
    )
    invite_path = reverse('invitation.accept', kwargs={'token': token})
    invite_url = request.build_absolute_uri(invite_path)
    expires_label = expires_at.astimezone(ZoneInfo('America/New_York')).strftime(
        '%b %d, %Y at %I:%M %p %Z'
    )
    context = {'email': account.email, 'invite_url': invite_url, 'expires_label': expires_label}
    message = EmailMultiAlternatives(
        subject='You are invited to Tools',
        body=render_to_string('emails/invitation.txt', context),
        from_email=settings.DEFAULT_FROM_EMAIL,
        to=[account.email],
    )
    message.attach_alternative(render_to_string('emails/invitation.html', context), 'text/html')
    try:
        message.send(fail_silently=False)
    except Exception:
        transaction.set_rollback(True)
        return JsonResponse({'error': {'code': 'EMAIL_FAILED'}}, status=503)
    return JsonResponse(
        {'email': account.email, 'invite_url': invite_url, 'expires_at': expires_at.isoformat()},
        status=201,
    )


@require_http_methods(['GET', 'POST'])
@transaction.atomic
def accept_invitation(request, token):
    invitation = (
        Invitation.objects.select_related('account').filter(token_hash=_token_hash(token)).first()
    )
    valid = bool(
        invitation and invitation.accepted_at is None and invitation.expires_at > timezone.now()
    )
    if request.method == 'GET':
        return render(
            request,
            'AcceptInvitation',
            props={
                'valid': valid,
                'email': invitation.account.email if valid else '',
                'token': token,
                'expires_at': invitation.expires_at.isoformat() if valid else None,
            },
        )
    if not valid or invitation is None:
        return JsonResponse({'error': {'code': 'INVITATION_INVALID'}}, status=410)
    password = field(request, 'password')
    confirmation = field(request, 'password_confirmation')
    if password != confirmation:
        return JsonResponse(
            {'error': {'code': 'PASSWORDS_DIFFER', 'field': 'password_confirmation'}}, status=422
        )
    account = invitation.account
    try:
        validate_password(password, user=account)
    except ValidationError:
        return JsonResponse(
            {'error': {'code': 'PASSWORD_INVALID', 'field': 'password'}}, status=422
        )
    account.set_password(password)
    account.is_active = True
    account.save(update_fields=['password', 'is_active', 'updated_at'])
    invitation.accepted_at = timezone.now()
    invitation.save(update_fields=['accepted_at', 'updated_at'])
    django_login(request, account)
    return JsonResponse({'redirect': reverse('home')})


@admin_required
@require_http_methods(['POST'])
def change_role(request, account_id):
    account = get_object_or_404(_accounts(), pk=account_id)
    role = field(request, 'role')
    if role not in (AccountRole.ADMIN, AccountRole.VIEWER):
        return HttpResponseForbidden()
    account.role = role
    account.is_staff = role == AccountRole.ADMIN
    account.save(update_fields=['role', 'is_staff', 'updated_at'])
    return redirect(request.META.get('HTTP_REFERER') or 'users')


@admin_required
@require_http_methods(['POST'])
@transaction.atomic
def deactivate(request, account_id):
    account = get_object_or_404(_accounts(), pk=account_id)
    account.is_active = False
    account.save(update_fields=['is_active', 'updated_at'])
    close_sessions(account)
    return redirect(request.META.get('HTTP_REFERER') or 'users')
