import copy
import logging
import re

import django
from django import core
from django.conf import settings
from django.contrib.auth import get_user_model
from django.db import IntegrityError, transaction
from django.db.models import Prefetch
from django.utils.functional import cached_property

from app import constants
from app.models import Group, User
from django_scim import exceptions as scim_exceptions
from django_scim.adapters import SCIMGroup, SCIMUser
from django_scim.constants import SchemaURI
from django_scim.utils import get_group_adapter, get_group_model

logger = logging.getLogger(__name__)


class SCIMUser(SCIMUser):

    password_changed = False
    activity_changed = False

    def __init__(self, obj, request=None):
        super().__init__(obj, request)

        self._from_dict_copy = None

    @property
    def is_new_user(self):
        return not bool(self.obj.id)

    @property
    def display_name(self):
        """
        Return the displayName of the user per the SCIM spec.
        """
        if self.obj.first_name and self.obj.last_name:
            return f'{self.obj.first_name} {self.obj.last_name}'
        return self.obj.email

    @property
    def groups(self):
        """
        Return the groups of the user per the SCIM spec.
        """

        group_qs = Group.objects.filter(members__member=self.obj)
        scim_groups = [get_group_adapter()(g, self.request) for g in group_qs]

        dicts = []
        for group in scim_groups:
            d = {
                'value': group.scim_id,
                '$ref': group.location,
                'display': group.display_name,
            }
            dicts.append(d)

        return dicts

    @property
    def meta(self):
        """
        Return the meta object of the user per the SCIM spec.
        """
        d = {
            'resourceType': self.resource_type,
            'created': self.obj.create_ts.isoformat(timespec='milliseconds'),
            'lastModified': self.obj.update_ts.isoformat(timespec='milliseconds'),
            'location': self.location,
        }

        return d

    def to_dict(self):
        """
        Return a ``dict`` conforming to the SCIM User Schema,
        ready for conversion to a JSON object.
        """
        d = super().to_dict()
        d.update({
            'userName': self.obj.scim_username,
            constants.SCHEMA_URI_APP_USER: {
                'companyId': self.obj.company_id,
            },
        })

        return d

    def from_dict(self, d):
        """
        Consume a ``dict`` conforming to the SCIM User Schema, updating the
        internal user object with data from the ``dict``.

        Please note, the user object is not saved within this method. To
        persist the changes made by this method, please call ``.save()`` on the
        adapter. Eg::

            scim_user.from_dict(d)
            scim_user.save()
        """
        # Store dict for possible later use when saving user
        self._from_dict_copy = copy.deepcopy(d)

        self.obj.company_id = self.request.user.company_id

        self.parse_active(d.get('active'))

        self.obj.first_name = d.get('name', {}).get('givenName') or ''

        self.obj.last_name = d.get('name', {}).get('familyName') or ''

        self.parse_email(d.get('emails'))

        if self.is_new_user and not self.obj.email:
            raise scim_exceptions.BadRequestError('Empty email value')

        self.obj.scim_username = d.get('userName')
        self.obj.scim_external_id = d.get('externalId') or ''

        cleartext_password = d.get('password')
        if cleartext_password:
            self.obj.set_password(cleartext_password)
            self.obj._scim_cleartext_password = cleartext_password
            self.password_changed = True

    def parse_active(self, active):
        if active is not None:
            if active != self.obj.is_active:
                self.activity_changed = True
            self.obj.is_active = active

    def parse_email(self, emails_value):
        if emails_value:
            email = None
            if isinstance(emails_value, list):
                primary_emails = [e['value'] for e in emails_value if e.get('primary')]
                other_emails = [e['value'] for e in emails_value if not e.get('primary')]
                # Make primary emails the first in the list
                sorted_emails = list(map(str.strip, primary_emails + other_emails))
                email = sorted_emails[0] if sorted_emails else None
            elif isinstance(emails_value, dict):
                # if value is a dict, let's assume it contains the primary email.
                # OneLogin sends a dict despite the spec:
                #   https://tools.ietf.org/html/rfc7643#section-4.1.2
                #   https://tools.ietf.org/html/rfc7643#section-8.2
                email = (emails_value.get('value') or '').strip()

            self.validate_email(email)

            self.obj.email = email

    @staticmethod
    def validate_email(email):
        try:
            validator = core.validators.EmailValidator()
            validator(email)
        except core.exceptions.ValidationError:
            raise scim_exceptions.BadRequestError('Invalid email value')

    def save(self):
        temp_password = None
        if self.is_new_user:
            password = getattr(self.obj, '_scim_cleartext_password', None)
            # If temp password was not passed, create one.
            if password is None:
                self.obj.require_password_change = True
                temp_password = generate_temp_password()
                password = temp_password
            self.obj.set_password(password)

        is_new_user = self.is_new_user
        try:
            with transaction.atomic():
                super().save()
                if is_new_user:
                    # Set SCIM ID to be equal to database ID. Because users are uniquely identified with this value
                    # its critical that changes to this line are well considered before executed.
                    self.obj.__class__.objects.update(scim_id=str(self.obj.id))
                logger.info(f'User saved. User id {self.obj.id}')
        except Exception as e:
            raise self.reformat_exception(e)

    def delete(self):
        self.obj.is_active = False
        self.obj.save()
        logger.info(f'Deactivated user id {self.obj.id}')

    def handle_add(self, path, value, operation):
        if path == 'externalId':
            self.obj.scim_external_id = value
            self.obj.save()

    def handle_replace(self, path, value, operation):
        """
        Handle the replace operations.

        All operations happen within an atomic transaction.
        """
        attr_map = {
            'familyName': 'last_name',
            'givenName': 'first_name',
            'active': 'is_active',
            'userName': 'scim_username',
            'externalId': 'scim_external_id',
        }

        for attr, attr_value in (value or {}).items():
            if attr in attr_map:
                setattr(self.obj, attr_map.get(attr), attr_value)

            elif attr == 'emails':
                self.parse_email(attr_value)

            elif attr == 'password':
                self.obj.set_password(attr_value)

            else:
                raise scim_exceptions.SCIMException('Not Implemented', status=409)

        self.obj.save()

    @classmethod
    def resource_type_dict(cls, request=None):
        d = super().resource_type_dict(request)
        d['schemaExtensions'] = [
            {
                'schema': constants.SCHEMA_URI_APP_USER,
                'required': False,
            },
        ]
        return d
