import logging

from django.conf import settings
from django.db.models import Q

from staff.lib.db import atomic
from staff.map.models import Office
from staff.rfid.models import Badge, BadgeChange
from staff.person.models import AFFILIATION, Staff
from staff.rfid.constants import OWNER, STATE, REASON
from staff.rfid.exceptions import (
    DoesNotExist,
    FieldIsRequired,
    RfidCodeBusyError,
    RfidBadgeDeleteError,
    RfidCodeRewriteError,
    RfidPersonRewriteError,
    RfidPersonHasBadgeError,
    RfidCodeCannotBeUsedError,
    RfidUnavailableStateTransition
)
from staff.person_avatar.storage import AvatarStorage, AvatarStorageError, AvatarBadRequestError, AvatarConnectingError
from staff.rfid.controllers.base import NoneField, EditableField, ReadOnlyField, BaseController, ListController
from staff.rfid.controllers.reserve import Reserves

logger = logging.getLogger('rfid.controllers.badge')


class CodeField(EditableField):

    def __init__(self, **kwargs):
        super(CodeField, self).__init__('rfid__code', **kwargs)

    def __set__(self, instance, value):
        if self.value_need_set(instance, value):
            reserve = self.get_reserve(instance, value)
            self.set_reserve(instance, reserve)

    @staticmethod
    def value_need_set(instance, value):
        if instance.obj.rfid:
            if instance.obj.rfid.code == value:
                return False
            else:
                raise RfidCodeRewriteError
        elif not value:
            # Не надо пытаться создавать сущность с пустым кодом.
            return False
        return True

    @staticmethod
    def get_reserve(instance, value):
        reserve, created = Reserves().get_or_create(code=value)
        return reserve

    @staticmethod
    def set_reserve(instance, reserve):
        instance.obj.rfid = reserve.obj
        if instance.state == STATE.NOCODE:
            instance.obj.state = STATE.ACTIVE


class FullNameField(ReadOnlyField):

    def __init__(self, prefix=''):
        super(FullNameField, self).__init__('full_name')
        self.prefix = prefix

    def __get__(self, instance, owner):
        return '{0} {1}'.format(
            instance._getattr('{0}first_name'.format(self.prefix), i18n=True),
            instance._getattr('{0}last_name'.format(self.prefix), i18n=True),
        )


class PersonField(EditableField):
    def __init__(self, **kwargs):
        super(PersonField, self).__init__('person', **kwargs)

    def _check_if_person_already_has_badge(self, instance, value: Staff):
        try:
            badges = Badges()
            if instance.id:
                badges.exclude(id=instance.id)
            badges.get(owner=OWNER.EMPLOYEE, state__in=[STATE.ACTIVE, STATE.NOCODE], person=value)
        except DoesNotExist:
            return

        raise RfidPersonHasBadgeError

    def __set__(self, instance, value: Staff):
        if not isinstance(value, Staff):
            raise TypeError('Person must be Staff instance.')

        current_person = instance._getattr(self.field_name)
        if current_person:
            if current_person == value:
                return
            else:
                raise RfidPersonRewriteError

        self._check_if_person_already_has_badge(instance, value)
        instance.obj.person = value


class BadgeController(BaseController):

    unavailable_states = [
        STATE.LOST,
        STATE.INACTIVE,
        STATE.DISCHARGED,
        STATE.BLOCKED,
    ]

    id = ReadOnlyField('id')
    created_at = ReadOnlyField('created_at')
    updated_at = ReadOnlyField('updated_at')
    changed_by = ReadOnlyField('changed_by')

    code = CodeField()
    owner = ReadOnlyField('owner')
    state = ReadOnlyField('state')
    reason = ReadOnlyField('reason')

    person = NoneField()
    guid = NoneField()
    contractor = NoneField()
    contractor_id = NoneField()
    contractor_name = NoneField()

    full_name = FullNameField()
    first_name = EditableField('first_name')
    last_name = EditableField('last_name')
    first_name_en = NoneField()
    last_name_en = NoneField()

    preprofile = NoneField()
    login = NoneField()
    photo = NoneField()
    office_id = NoneField()

    _inactive_state = STATE.INACTIVE
    _inactive_reason = REASON.INACTIVE

    def __init__(self, *args, **kwargs):
        self.author = kwargs.pop('author', None)
        super(BadgeController, self).__init__(*args, **kwargs)

    def block(self):
        if self.state in (STATE.NOCODE, STATE.ACTIVE):
            raise RfidUnavailableStateTransition
        self.obj.state = STATE.BLOCKED
        self.save()

    def create(self, owner, **kwargs):
        self.obj.owner = owner
        code = kwargs.get('code', None)
        self.obj.state = STATE.NOCODE if code is None else STATE.ACTIVE
        return super(BadgeController, self).create(**kwargs)

    def activate(self):
        if self.state != STATE.BLOCKED or (
            self.person is None and self.owner == OWNER.CANDIDATE
        ):
            raise RfidUnavailableStateTransition

        self.obj.state = STATE.ACTIVE
        self.obj.reason = None
        self.save()

    def deactivate(self):
        if self.state in self.unavailable_states:
            raise RfidUnavailableStateTransition

        self.obj.state = self._inactive_state
        if self._inactive_reason:
            self.obj.reason = self._inactive_reason
        self.save()

    @atomic
    def delete(self):
        if self.state != STATE.NOCODE:
            raise RfidBadgeDeleteError
        if self.obj.state != STATE.NOCODE and self.obj.owner in [OWNER.EMPLOYEE, OWNER.ANONYM]:
            BadgeChange.objects.create(
                badge_id=self.obj.id,
                code=self.obj.rfid.code,
                person=self.obj.person,
                is_sent=False,
                state=STATE.INACTIVE,
            )
        self.obj.delete()

    def save(self):
        self.obj.changed_by = self.author
        super(BadgeController, self).save()


class AnonymCodeField(CodeField):
    @staticmethod
    def get_reserve(instance, value):
        reserve, created = Reserves().get_or_create(code=value)
        if not created:
            raise RfidCodeBusyError
        return reserve


class CandidateCodeField(CodeField):
    @staticmethod
    def get_reserve(instance, value):
        reserve, created = Reserves().get_or_create(code=value)
        if not created:
            try:
                assert Office.objects.get(
                    pk=instance.office_id).city.country_id == settings.RUSSIA_ID
            except (AssertionError, Office.DoesNotExist):
                raise RfidCodeCannotBeUsedError
        return reserve


class EmployeeCodeField(CodeField):
    @staticmethod
    def get_reserve(instance, value):
        reserve, created = Reserves().get_or_create(code=value)
        if not created:
            try:
                assert instance.person.affiliation == AFFILIATION.YANDEX
                assert instance.person.office.city.country_id == settings.RUSSIA_ID
            except (AssertionError, AttributeError):
                raise RfidCodeCannotBeUsedError
        return reserve


class Anonym(BadgeController):
    AVATAR_SCOPE = 'staff'
    avatar_storage_class = AvatarStorage
    _inactive_reason = None

    def __init__(self, *args, **kwargs):
        avatar_storage = kwargs.pop('avatar_storage', None)
        super(Anonym, self).__init__(*args, **kwargs)
        self.photo_file = None
        self.avatar_storage = (
            avatar_storage or self.avatar_storage_class(self.AVATAR_SCOPE)
        )

    @classmethod
    def condition(cls, obj):
        if isinstance(obj, dict):
            owner = obj.get('owner')
        else:
            owner = getattr(obj, 'owner')
        return owner == OWNER.ANONYM

    code = AnonymCodeField()
    contractor = EditableField('contractor')
    contractor_name = ReadOnlyField('contractor__name', i18n=True)
    contractor_id = ReadOnlyField('contractor_id')
    anonym_food_allowed = EditableField('anonym_food_allowed')

    def create(self, **kwargs):
        photo_file = kwargs.pop('photo_file', None)
        self.photo_file = photo_file
        return super(Anonym, self).create(**kwargs)

    def save(self):
        if not self.first_name:
            raise FieldIsRequired('first_name')

        if not self.last_name:
            raise FieldIsRequired('last_name')

        if not self.id and self.photo_file is None:
            raise FieldIsRequired('photo_file')

        super(Anonym, self).save()

        if self.photo_file is not None:
            try:
                self.avatar_storage.upload_by_file(f'rfid-{self.id}', self.photo_file)
            except (AvatarStorageError, AvatarBadRequestError, AvatarConnectingError) as e:
                logger.error('Error trying to upload photo. %s', str(e))


class CandidateBadgeController(BadgeController):

    @classmethod
    def condition(cls, obj):
        if isinstance(obj, dict):
            owner = obj.get('owner')
        else:
            owner = getattr(obj, 'owner')
        return owner == OWNER.CANDIDATE

    preprofile_id = EditableField('preprofile_id')
    photo = EditableField('photo')
    office_id = EditableField('office_id')
    first_name_en = EditableField('first_name_en')
    last_name_en = EditableField('last_name_en')
    login = EditableField('login')

    code = CandidateCodeField()

    def to_employee(self, person):
        self.obj.owner = OWNER.EMPLOYEE
        employee = EmployeeBadgeController(self.obj)
        employee.person = person
        employee.save()
        return employee


class EmployeeBadgeController(BadgeController):

    _inactive_state = STATE.LOST
    _inactive_reason = REASON.LOST

    @classmethod
    def condition(cls, obj):
        if isinstance(obj, dict):
            owner = obj.get('owner')
        else:
            owner = getattr(obj, 'owner')
        return owner == OWNER.EMPLOYEE

    full_name = FullNameField(prefix='person__')
    first_name = ReadOnlyField('person__first_name')
    last_name = ReadOnlyField('person__last_name')
    first_name_en = ReadOnlyField('person__first_name_en')
    last_name_en = ReadOnlyField('person__last_name_en')
    middle_name = ReadOnlyField('person__middle_name')
    login = ReadOnlyField('person__login')
    guid = ReadOnlyField('person__guid')

    person = PersonField()

    code = EmployeeCodeField()

    def save(self):
        if self.person is None:
            raise FieldIsRequired('person')

        super().save()

    def dismiss(self):
        unavailable_states = [
            STATE.LOST,
            STATE.INACTIVE,
            STATE.DISCHARGED,
            STATE.BLOCKED,
        ]
        if self.state in unavailable_states:
            raise RfidUnavailableStateTransition

        self.obj.state = STATE.DISCHARGED
        self.obj.reason = REASON.DISCHARGED
        self.save()


class Badges(ListController):

    model = Badge
    controllers = EmployeeBadgeController, Anonym, CandidateBadgeController

    def __init__(self, author=None):
        super(Badges, self).__init__()
        self.ctl_params['author'] = author

    def values(self, *fields):
        self._requested_fields = list(fields)
        q_fields = set(fields)

        if 'owner' not in q_fields:
            q_fields.add('owner')

        if any(f in q_fields for f in [
            'full_name',
            'first_name',
            'first_name_en'
        ]):
            q_fields.add('first_name')
            q_fields.add('first_name_en')
            q_fields.add('person__first_name')
            q_fields.add('person__first_name_en')

        if any(f in q_fields for f in [
            'full_name',
            'last_name',
            'last_name_en',
        ]):
            q_fields.add('last_name')
            q_fields.add('last_name_en')
            q_fields.add('person__last_name')
            q_fields.add('person__last_name_en')

        if 'middle_name' in q_fields:
            q_fields.add('person__middle_name')
            q_fields.remove('middle_name')

        if 'code' in q_fields:
            q_fields.add('rfid__code')
        if 'login' in q_fields:
            q_fields.add('person__login')
        if 'guid' in q_fields:
            q_fields.add('person__guid')
            q_fields.discard('guid')
        if 'contractor_name' in q_fields:
            q_fields.add('contractor__name')
            q_fields.add('contractor__name_en')
            q_fields.discard('contractor_name')

        q_fields.discard('code')
        q_fields.discard('full_name')
        super(Badges, self).values(*q_fields)
        return self

    def search(self, search_str):
        if not search_str:
            return self

        search_fields = [
            'rfid__code',
            'person__first_name',
            'person__first_name_en',
            'person__last_name',
            'person__last_name_en',
            'person__login',
            'login',
            'first_name',
            'last_name',
            'first_name_en',
            'last_name_en',
            'contractor__name',
            'contractor__name_en',
        ]

        for bit in search_str.split():
            or_queries = None
            for search_field in search_fields:
                q = Q(**{search_field + '__icontains': bit})
                if or_queries is None:
                    or_queries = q
                else:
                    or_queries |= q
            self.qs = self.qs.filter(or_queries)

        return self

    def create_employee(self, person, **kwargs):
        return self.create(person=person, owner=OWNER.EMPLOYEE, **kwargs)

    def create_candidate(self, **kwargs):
        return self.create(owner=OWNER.CANDIDATE, **kwargs)

    def create_anonym(self, **kwargs):
        return self.create(owner=OWNER.ANONYM, **kwargs)

    def get_query_set(self):
        return (
            super(Badges, self).get_query_set()
            .select_related('rfid', 'contractor', 'person', 'preprofile')
        )
