from decimal import Decimal

from aws_lambda_powertools import Logger
from aws_lambda_powertools.event_handler.exceptions import (
    BadRequestError,
    NotFoundError,
)
from layercake.dateutils import now, ttl
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair, SortKey, TransactKey
from layercake.strutils import md5_hash

from config import COURSE_TABLE, DEDUP_WINDOW_OFFSET_DAYS

logger = Logger(__name__)


def update_progress(
    id: str,
    progress: Decimal,
    *,
    dynamodb_persistence_layer: DynamoDBPersistenceLayer,
):
    now_ = now()
    reminder_ttl = ttl(start_dt=now_, days=7)

    try:
        with dynamodb_persistence_layer.transact_writer() as transact:
            # Update progress only if the enrollment status is `IN_PROGRESS`
            transact.update(
                key=KeyPair(id, '0'),
                update_expr='SET progress = :progress, \
                                 updated_at = :now',
                cond_expr='#status = :in_progress',
                expr_attr_names={
                    '#status': 'status',
                },
                expr_attr_values={
                    ':in_progress': 'IN_PROGRESS',
                    ':progress': progress,
                    ':now': now_,
                },
                exc_cls=EnrollmentConflictError,
            )
            # Schedule a reminder if there is no activity after 7 days
            transact.put(
                item={
                    'id': id,
                    'sk': 'SCHEDULE#REMINDER_NO_ACTIVITY_AFTER_7_DAYS',
                    'ttl': reminder_ttl,
                    'created_at': now_,
                }
            )
    except EnrollmentConflictError:
        with dynamodb_persistence_layer.transact_writer() as transact:
            # If the enrollment status is `PENDING`, set it to `IN_PROGRESS`
            # and update `progress` and `updated_at`
            transact.update(
                key=KeyPair(id, '0'),
                update_expr='SET progress = :progress, \
                                 #status = :in_progress, \
                                 started_at = if_not_exists(started_at, :now), \
                                 updated_at = :now',
                cond_expr='#status = :pending',
                expr_attr_names={
                    '#status': 'status',
                },
                expr_attr_values={
                    ':in_progress': 'IN_PROGRESS',
                    ':pending': 'PENDING',
                    ':progress': progress,
                    ':now': now_,
                },
                exc_cls=EnrollmentConflictError,
            )
            # Schedule a reminder for inactivity
            transact.put(
                item={
                    'id': id,
                    'sk': 'SCHEDULE#REMINDER_NO_ACTIVITY_AFTER_7_DAYS',
                    'ttl': reminder_ttl,
                    'created_at': now_,
                }
            )
            # Remove reminders and policies that no longer apply
            transact.delete(
                key=KeyPair(
                    pk=id,
                    sk='CANCEL_POLICY',
                ),
            )
            transact.delete(
                key=KeyPair(
                    pk=id,
                    sk='SCHEDULE#REMINDER_NO_ACCESS_AFTER_3_DAYS',
                )
            )

    return True


def set_score(
    id: str,
    /,
    score: Decimal,
    progress: Decimal,
    *,
    dynamodb_persistence_layer: DynamoDBPersistenceLayer,
):
    enrollment = dynamodb_persistence_layer.collection.get_items(
        TransactKey(id)
        + SortKey('0')
        + SortKey(
            sk='METADATA#DEDUPLICATION_WINDOW',
            path_spec='offset_days',
            rename_key='dedup_window_offset_days',
        ),
    )
    user_id = enrollment['user']['id']
    course_id = enrollment['course']['id']
    dedup_window_offset_days = int(
        enrollment.get('dedup_window_offset_days', DEDUP_WINDOW_OFFSET_DAYS)
    )

    try:
        if score >= 70:
            # Got a score of 70 or higher
            return _set_status_as_completed(
                id,
                score,
                progress=progress,
                user_id=user_id,
                course_id=course_id,
                dedup_window_offset_days=dedup_window_offset_days,
                dynamodb_persistence_layer=dynamodb_persistence_layer,
            )

        # Got a score below 70
        return _set_status_as_failed(
            id,
            score,
            progress=progress,
            user_id=user_id,
            course_id=course_id,
            dynamodb_persistence_layer=dynamodb_persistence_layer,
        )
    except EnrollmentConflictError as err:
        logger.exception(err)
        raise


def _set_status_as_completed(
    id: str,
    /,
    score: Decimal,
    progress: Decimal,
    *,
    user_id: str,
    course_id: str,
    dedup_window_offset_days: int,
    dynamodb_persistence_layer: DynamoDBPersistenceLayer,
) -> bool:
    now_ = now()
    lock_hash = md5_hash(f'{user_id}{course_id}')
    cert_exp_interval = int(
        dynamodb_persistence_layer.collection.get_item(
            KeyPair(
                pk=course_id,
                sk=SortKey('0', path_spec='cert.exp_interval'),
                table_name=COURSE_TABLE,
            ),
            raise_on_error=False,
            default=0,
        )
    )

    with dynamodb_persistence_layer.transact_writer() as transact:
        transact.update(
            key=KeyPair(pk=id, sk='0'),
            update_expr='SET #status = :completed, \
                             progress = :progress, \
                             score = :score, \
                             completed_at = if_not_exists(completed_at, :now), \
                             updated_at = :now',
            cond_expr='#status = :in_progress',
            expr_attr_names={'#status': 'status'},
            expr_attr_values={
                ':completed': 'COMPLETED',
                ':in_progress': 'IN_PROGRESS',
                ':score': score,
                ':progress': progress,
                ':now': now_,
            },
            exc_cls=EnrollmentConflictError,
        )

        if cert_exp_interval:
            dedup_lock_ttl = ttl(
                start_dt=now_,
                days=cert_exp_interval - dedup_window_offset_days,
            )

            transact.put(
                item={
                    'id': id,
                    'sk': 'SCHEDULE#REMINDER_CERT_EXPIRED',
                    'ttl': ttl(start_dt=now_, days=cert_exp_interval),
                    'created_at': now_,
                }
            )
            transact.put(
                item={
                    'id': id,
                    'sk': 'SCHEDULE#REMINDER_CERT_EXPIRATION_BEFORE_30_DAYS',
                    'ttl': ttl(start_dt=now_, days=cert_exp_interval - 30),
                    'created_at': now_,
                }
            )
            transact.put(
                item={
                    'id': id,
                    'sk': 'LOCK',
                    'ttl': dedup_lock_ttl,
                    'hash': lock_hash,
                    'created_at': now_,
                }
            )
            transact.put(
                item={
                    'id': 'LOCK',
                    'sk': lock_hash,
                    'enrollment_id': id,
                    'ttl': dedup_lock_ttl,
                    'created_at': now_,
                }
            )
        else:
            transact.put(
                item={
                    'id': id,
                    'sk': 'LOCK',
                    'hash': lock_hash,
                    'created_at': now_,
                }
            )
            transact.put(
                item={
                    'id': 'LOCK',
                    'sk': lock_hash,
                    'enrollment_id': id,
                    'created_at': now_,
                }
            )

        # Remove reminders and policies that no longer apply
        transact.delete(
            key=KeyPair(
                pk=id,
                sk='CANCEL_POLICY',
            )
        )
        transact.delete(
            key=KeyPair(
                pk=id,
                sk='SCHEDULE#REMINDER_NO_ACTIVITY_AFTER_7_DAYS',
            )
        )
        transact.delete(
            key=KeyPair(
                pk=id,
                sk='SCHEDULE#REMINDER_ACCESS_PERIOD_BEFORE_30_DAYS',
            )
        )

    return True


def _set_status_as_failed(
    id: str,
    /,
    score: Decimal,
    progress: Decimal,
    user_id: str,
    course_id: str,
    *,
    dynamodb_persistence_layer: DynamoDBPersistenceLayer,
) -> bool:
    now_ = now()
    lock_hash = md5_hash(f'{user_id}{course_id}')

    with dynamodb_persistence_layer.transact_writer() as transact:
        transact.update(
            key=KeyPair(pk=id, sk='0'),
            update_expr='SET #status = :failed, \
                             progress = :progress, \
                             score = :score, \
                             access_expired = :true, \
                             failed_at = if_not_exists(failed_at, :now), \
                             updated_at = :now',
            cond_expr='#status = :in_progress',
            expr_attr_names={'#status': 'status'},
            expr_attr_values={
                ':failed': 'FAILED',
                ':in_progress': 'IN_PROGRESS',
                ':score': score,
                ':progress': progress,
                ':true': True,
                ':now': now_,
            },
            exc_cls=EnrollmentConflictError,
        )
        transact.put(
            item={
                'id': id,
                'sk': 'FAILED',
                'created_at': now_,
            },
            cond_expr='attribute_not_exists(sk)',
        )
        # Remove reminders and events that no longer apply
        transact.delete(
            key=KeyPair(
                pk=id,
                sk='CANCEL_POLICY',
            )
        )
        transact.delete(
            key=KeyPair(
                pk=id,
                sk='SCHEDULE#REMINDER_NO_ACTIVITY_AFTER_7_DAYS',
            )
        )
        transact.delete(
            key=KeyPair(
                pk=id,
                sk='SCHEDULE#REMINDER_ACCESS_PERIOD_BEFORE_30_DAYS',
            )
        )
        # Remove locks related to this enrollment
        transact.delete(
            key=KeyPair(pk=id, sk='LOCK'),
        )
        transact.delete(
            key=KeyPair(pk='LOCK', sk=lock_hash),
        )

    return True


class EnrollmentNotFoundError(NotFoundError):
    def __init__(self, *_):
        super().__init__('Enrollment not found')


class EnrollmentConflictError(BadRequestError):
    def __init__(self, *_):
        super().__init__('Enrollment status conflict')
