from datetime import timedelta
from types import SimpleNamespace
from typing import TypedDict

from aws_lambda_powertools.event_handler.exceptions import (
    BadRequestError,
)
from layercake.dateutils import now, ttl
from layercake.dynamodb import (
    ComposeKey,
    DynamoDBPersistenceLayer,
    KeyPair,
    SortKey,
)

User = TypedDict('User', {'id': str, 'name': str, 'cpf': str})


def update_user(
    data: User,
    /,
    *,
    persistence_layer: DynamoDBPersistenceLayer,
) -> bool:
    now_ = now()
    user = SimpleNamespace(**data)
    # Get the user's CPF, if it exists.
    old_cpf = persistence_layer.collection.get_item(
        KeyPair(
            pk=user.id,
            sk=SortKey('0', path_spec='cpf'),
        )
    )

    with persistence_layer.transact_writer() as transact:
        transact.update(
            key=KeyPair(user.id, '0'),
            update_expr='SET #name = :name, cpf = :cpf, update_date = :update_date',
            expr_attr_names={
                '#name': 'name',
            },
            expr_attr_values={
                ':name': user.name,
                ':cpf': user.cpf,
                ':update_date': now_,
            },
            cond_expr='attribute_exists(sk)',
        )

        class RateLimitError(BadRequestError):
            def __init__(self, msg: str):
                super().__init__('Update limit reached')

        # Prevent the user from updating more than once every 24 hours
        transact.put(
            item={
                'id': user.id,
                'sk': 'rate_limit#user_update',
                'create_date': now_,
                'ttl': ttl(start_dt=now_ + timedelta(hours=24)),
            },
            exc_cls=RateLimitError,
            cond_expr='attribute_not_exists(sk)',
        )

        class CPFConflictError(BadRequestError):
            def __init__(self, msg: str):
                super().__init__('CPF already exists')

        if user.cpf != old_cpf:
            transact.put(
                item={
                    'id': 'cpf',
                    'sk': user.cpf,
                    'user_id': user.id,
                    'create_date': now_,
                },
                cond_expr='attribute_not_exists(sk)',
                exc_cls=CPFConflictError,
            )

        # Ensures that the old CPF is discarded
        if old_cpf:
            transact.delete(key=KeyPair('cpf', old_cpf))

    return True


def add_email(
    id: str,
    email: str,
    /,
    *,
    persistence_layer: DynamoDBPersistenceLayer,
):
    now_ = now()

    with persistence_layer.transact_writer() as transact:
        # Ensure email is searchable
        transact.update(
            key=KeyPair(id, '0'),
            update_expr='ADD emails :email',
            expr_attr_values={
                ':email': {email},
            },
        )
        transact.put(
            item={
                'id': id,
                'sk': ComposeKey(email, prefix='emails'),
                'email_primary': False,
                'email_verified': False,
                'create_date': now_,
            },
            cond_expr='attribute_not_exists(sk)',
        )

        class EmailConflictError(BadRequestError):
            def __init__(self, msg: str):
                super().__init__('Email already exists')

        # Prevent duplicate emails
        transact.put(
            item={
                'id': 'email',
                'sk': email,
                'user_id': id,
                'create_date': now_,
            },
            cond_expr='attribute_not_exists(sk)',
            exc_cls=EmailConflictError,
        )

    return True


def del_email(
    id: str,
    email: str,
    /,
    *,
    persistence_layer: DynamoDBPersistenceLayer,
) -> bool:
    """Delete any email except the primary email."""
    with persistence_layer.transact_writer() as transact:
        transact.delete(key=KeyPair('email', email))
        transact.delete(
            key=KeyPair(id, ComposeKey(email, prefix='emails')),
            cond_expr='email_primary <> :email_primary',
            expr_attr_values={
                ':email_primary': True,
            },
            exc_cls=BadRequestError,
        )
        transact.update(
            key=KeyPair(id, '0'),
            update_expr='DELETE emails :email',
            expr_attr_values={
                ':email': {email},
            },
        )
    return True


def set_email_as_primary(
    id: str,
    new_email: str,
    old_email: str,
    /,
    *,
    email_verified: bool = False,
    persistence_layer: DynamoDBPersistenceLayer,
):
    now_ = now()
    expr = 'SET email_primary = :email_primary, updated_at = :updated_at'

    with persistence_layer.transact_writer() as transact:
        # Set the old email as non-primary
        transact.update(
            key=KeyPair(id, ComposeKey(old_email, prefix='emails')),
            update_expr=expr,
            expr_attr_values={
                ':email_primary': False,
                ':updated_at': now_,
            },
        )
        # Set the new email as primary
        transact.update(
            key=KeyPair(id, ComposeKey(new_email, 'emails')),
            update_expr=expr,
            expr_attr_values={
                ':email_primary': True,
                ':updated_at': now_,
            },
        )
        transact.update(
            key=KeyPair(id, '0'),
            update_expr='DELETE emails :email_set SET email = :email, \
                        email_verified = :email_verified, updated_at = :updated_at',
            expr_attr_values={
                ':email': new_email,
                ':email_set': {new_email},
                ':email_verified': email_verified,
                ':updated_at': now_,
            },
        )

    return True


def del_org_member(
    id: str,
    *,
    org_id: str,
    persistence_layer: DynamoDBPersistenceLayer,
) -> bool:
    with persistence_layer.transact_writer() as transact:
        # Remove the user's relationship with the organization and their privileges
        transact.delete(key=KeyPair(id, ComposeKey(org_id, prefix='acls')))
        transact.delete(key=KeyPair(id, ComposeKey(org_id, prefix='orgs')))
        transact.update(
            key=KeyPair(id, '0'),
            update_expr='DELETE #tenant :org_id',
            expr_attr_names={'#tenant': 'tenant__org_id'},
            expr_attr_values={':org_id': {org_id}},
        )

        # Remove the user from the organization's admins and members list
        transact.delete(key=KeyPair(org_id, ComposeKey(id, prefix='admins')))
        transact.delete(key=KeyPair(ComposeKey(org_id, prefix='orgmembers'), id))

    return True
