import logging
from abc import ABC, abstractmethod
from typing import Mapping, TypeVar, Generic

from marshmallow import ValidationError
from sqlalchemy.orm import with_polymorphic

from pycroft.lib.logging import log_task_event
from pycroft.model import session
from pycroft.model.session import with_transaction
from pycroft.model.task import UserTask, Task, TaskType, TaskStatus
from pycroft.model.task_serialization import UserMoveOutParams, UserMoveParams, UserMoveInParams, \
    TaskParams
from pycroft.model.user import User

TTask = TypeVar('TTask')
TParams = TypeVar('TParams')

logger = logging.getLogger('pycroft.task')


class TaskImpl(ABC, Generic[TTask, TParams]):
    @property
    @abstractmethod
    def name(self):
        ...

    @property
    @abstractmethod
    def type(self):
        ...

    new_status = None
    errors = list()

    @abstractmethod
    @with_transaction
    def schedule(self, *args, **kwargs):
        ...

    @with_transaction
    def execute(self, task: Task):
        self.new_status = TaskStatus.FAILED
        self.errors = list()

        try:
            parameters: TParams = task.parameters
        except ValidationError as e:
            self.errors.append(f"Failed to parse parameters: {e.messages}")
            logger.error('Failed to deserialize parameters', exc_info=True)
        else:
            self._execute(task, parameters)

    @abstractmethod
    def _execute(self, task: TTask, parameters: TParams):
        ...


class UserTaskImpl(TaskImpl, ABC):
    def schedule(self, due, user, parameters, processor):
        if due < session.utcnow():
            raise ValueError("the due date must be in the future")

        task = UserTask(type=self.type,
                        due=due,
                        creator=processor,
                        created=session.utcnow(),
                        user=user)

        task.parameters = parameters

        return task


class UserMoveOutTaskImpl(UserTaskImpl):
    name = "Auszug"
    type = TaskType.USER_MOVE_OUT

    def _execute(self, task: UserTask, parameters: UserMoveOutParams):
        from pycroft.lib import user as lib_user
        if task.user.room is None:
            self.errors.append("Tried to move out user, but user was not living in a dormitory")
            return

        lib_user.move_out(user=task.user,
                          comment=parameters.comment,
                          processor=task.creator,
                          when=session.utcnow(),
                          end_membership=parameters.end_membership)

        self.new_status = TaskStatus.EXECUTED


class UserMoveTaskImpl(UserTaskImpl):
    name = "Umzug"
    type = TaskType.USER_MOVE

    def _execute(self, task, parameters: UserMoveParams):
        from pycroft.lib import user as lib_user
        if task.user.room is None:
            self.errors.append("Tried to move in user, "
                               "but user was already living in a dormitory.")
            return

        room = lib_user.get_room(
            room_number=parameters.room_number,
            level=parameters.level,
            building_id=parameters.building_id,
        )

        if room is None:
            self.errors.append("Tried to move user, but target room did not exist.")
            return

        lib_user.move(
            user=task.user,
            building_id=room.building.id,
            level=room.level,
            room_number=room.number,
            comment=parameters.comment,
            processor=task.creator,
        )

        self.new_status = TaskStatus.EXECUTED


class UserMoveInTaskImpl(UserTaskImpl):
    name = "Einzug"
    type = TaskType.USER_MOVE_IN

    def _execute(self, task, parameters: UserMoveInParams):
        from pycroft.lib import user as lib_user

        if task.user.room is not None:
            self.errors.append("Tried to move in user, "
                               "but user was already living in a dormitory.")
            return

        room = lib_user.get_room(
            room_number=parameters.room_number,
            level=parameters.level,
            building_id=parameters.building_id,
        )

        if room is None:
            self.errors.append(
                "Tried to move in user, but target room did not exist.")
            return

        lib_user.move_in(user=task.user,
                         building_id=room.building.id,
                         level=room.level,
                         room_number=room.number,
                         mac=parameters.mac,
                         processor=task.creator,
                         birthdate=parameters.birthdate,
                         host_annex=parameters.host_annex,
                         begin_membership=parameters.begin_membership,
                         )

        self.new_status = TaskStatus.EXECUTED


task_type_to_impl: Mapping[TaskType, type[UserTaskImpl]] = {
    TaskType.USER_MOVE: UserMoveTaskImpl,
    TaskType.USER_MOVE_IN: UserMoveInTaskImpl,
    TaskType.USER_MOVE_OUT: UserMoveOutTaskImpl
}


def get_task_implementation(task: Task) -> TaskImpl:
    return task_type_to_impl.get(task.type)()


@with_transaction
def schedule_user_task(task_type, due, user, parameters: TaskParams, processor):
    if due < session.utcnow():
        raise ValueError("the due date must be in the future")

    task = UserTask(type=task_type,
                    due=due,
                    creator=processor,
                    created=session.utcnow(),
                    user=user)

    task.parameters = parameters
    session.session.add(task)

    return task


def get_active_tasks_by_type(type):
    task_and_subtypes = with_polymorphic(Task, "*")
    return session.session.query(
        task_and_subtypes.where(task_and_subtypes.type == type)
    ).all()


@with_transaction
def cancel_task(task, processor):
    if task.status != TaskStatus.OPEN:
        raise ValueError("Cannot cancel a task that is not open")

    log_task_event("Cancelled task {}.".format(task.id), processor, task)

    task.status = TaskStatus.CANCELLED


def force_execute_task(task: Task, processor: User):
    if task.status != TaskStatus.OPEN:
        raise ValueError("Cannot execute a task that is not open")

    pass  # not implemented

@@ -5875,16 +5875,22 @@
 ask(task
+: Task
 , proces
@@ -5884,32 +5884,38 @@
  Task, processor
+: User
 ):%0A    if task.s

Add type hints to `lib.task.cancel_task`