import datetime
from sqlalchemy.orm.exc import NoResultFound

from atst.database import db
from atst.models import ApplicationInvitation, InvitationStatus, PortfolioInvitation
from atst.domain.portfolio_roles import PortfolioRoles
from atst.domain.application_roles import ApplicationRoles

from .exceptions import NotFoundError


class WrongUserError(Exception):
    def __init__(self, user, invite):
        self.user = user
        self.invite = invite

    @property
    def message(self):
        return "User {} with DOD ID {} does not match expected DOD ID {} for invitation {}".format(
            self.user.id, self.user.dod_id, self.invite.user.dod_id, self.invite.id
        )


class ExpiredError(Exception):
    def __init__(self, invite):
        self.invite = invite

    @property
    def message(self):
        return "Invitation {} has expired.".format(self.invite.id)


class InvitationError(Exception):
    def __init__(self, invite):
        self.invite = invite

    @property
    def message(self):
        return "{} has a status of {}".format(self.invite.id, self.invite.status.value)


class BaseInvitations(object):
    model = None
    role_domain_class = None
    # number of minutes a given invitation is considered valid
    EXPIRATION_LIMIT_MINUTES = 360

    @classmethod
    def _get(cls, token):
        try:
            invite = db.session.query(cls.model).filter_by(token=token).one()
        except NoResultFound:
            raise NotFoundError(cls.model.__tablename__)

        return invite

    @classmethod
    def create(cls, inviter, role, member_data, commit=False):
        # pylint: disable=not-callable
        invite = cls.model(
            role=role,
            inviter=inviter,
            user=role.user,
            status=InvitationStatus.PENDING,
            expiration_time=cls.current_expiration_time(),
            email=member_data.get("email"),
            dod_id=member_data.get("dod_id"),
            first_name=member_data.get("first_name"),
            phone_number=member_data.get("phone_number"),
            last_name=member_data.get("last_name"),
        )
        db.session.add(invite)

        if commit:
            db.session.commit()

        return invite

    @classmethod
    def accept(cls, user, token):
        invite = cls._get(token)

        if invite.dod_id != user.dod_id:
            if invite.is_pending:
                cls._update_status(invite, InvitationStatus.REJECTED_WRONG_USER)
            raise WrongUserError(user, invite)

        elif invite.is_expired:
            cls._update_status(invite, InvitationStatus.REJECTED_EXPIRED)
            raise ExpiredError(invite)

        elif invite.is_accepted or invite.is_revoked or invite.is_rejected:
            raise InvitationError(invite)

        elif invite.is_pending:  # pragma: no branch
            cls._update_status(invite, InvitationStatus.ACCEPTED)
            cls.role_domain_class.enable(invite.role, user)
            return invite

    @classmethod
    def current_expiration_time(cls):
        return datetime.datetime.now() + datetime.timedelta(
            minutes=cls.EXPIRATION_LIMIT_MINUTES
        )

    @classmethod
    def _update_status(cls, invite, new_status):
        invite.status = new_status
        db.session.add(invite)
        db.session.commit()

        return invite

    @classmethod
    def revoke(cls, token):
        invite = cls._get(token)
        invite = cls._update_status(invite, InvitationStatus.REVOKED)
        cls.role_domain_class.disable(invite.role)
        return invite

    @classmethod
    def resend(cls, inviter, token, user_info=None):
        previous_invitation = cls._get(token)
        cls._update_status(previous_invitation, InvitationStatus.REVOKED)

        if not user_info:
            user_info = {
                "email": previous_invitation.email,
                "dod_id": previous_invitation.dod_id,
                "first_name": previous_invitation.first_name,
                "last_name": previous_invitation.last_name,
                "phone_number": previous_invitation.phone_number,
                "phone_ext": previous_invitation.phone_ext,
            }

        return cls.create(inviter, previous_invitation.role, user_info, commit=True)


class PortfolioInvitations(BaseInvitations):
    model = PortfolioInvitation
    role_domain_class = PortfolioRoles


class ApplicationInvitations(BaseInvitations):
    model = ApplicationInvitation
    role_domain_class = ApplicationRoles
