"""This module declareds the pydantic schema representation for users.

Note that it is not a direct representation of how users are modeled in the
database. Instead, the User schema class contains all attributes from all user classes
as optional attributes.

I haven't figured out a smart way to do this with pydantic yet, so behold the
inheritance hellhole below.
"""

from datetime import datetime, date
from abc import ABC
from typing import Optional

from pydantic import BaseModel, validator

from backend.models.users import Gender
from backend.schemas.devices import Device


class AbstractUserInfoValidation(BaseModel, ABC):
    """Base class providing common field validators."""

    @validator('email', check_fields=False)
    def assert_email_is_valid(cls, email):
        if email is not None:
            if not len(email):
                raise ValueError("Email must not be empty.")
            # TODO implement more robust check
        return email

    @validator('first_name', check_fields=False)
    def assert_first_name_is_valid(cls, first_name):
        if first_name is not None:
            if not len(first_name):
                raise ValueError("First Name must not be empty.")
        return first_name

    @validator('last_name', check_fields=False)
    def assert_last_name_is_valid(cls, last_name):
        if last_name is not None:
            if not len(last_name):
                raise ValueError("Last Name must not be empty.")
        return last_name

    @validator('date_of_birth', check_fields=False)
    def assert_dob_is_valid(cls, dob):
        if dob is not None:
            if dob >= date.today():
                raise ValueError("Date of birth cannot be in the future.")
        return dob


class AbstractUser(AbstractUserInfoValidation, ABC):
    """Base class for attributes common to user creation and user representation.

    A user must be either a patient or an administrator. If a user is a patient,
    they must specify valid 'date_of_birth' and 'gender' attributes.
    """

    email: str
    first_name: str
    last_name: str

    gender: Optional[Gender]
    date_of_birth: Optional[date]
    is_patient: Optional[bool]

    is_admin: Optional[bool]

    @validator('is_admin')
    def assert_tegridy(cls, is_admin, values):
        """Ensures logical model integrity when optional fields are set."""

        if values['is_patient']:
            if is_admin:
                raise ValueError('User cannot be both patient and admin.')
            for key in ['gender', 'date_of_birth']:
                if key not in values or values[key] is None:
                    raise ValueError(f"Must specify key '{key}' for patients.")
        if not values['is_patient'] and not is_admin:
            raise ValueError(f'User must either be patient or admin.')
        return is_admin


class UserCreate(AbstractUser):
    """Scheme for user creation."""

    password: str
    password_confirmation: str

    @validator('password_confirmation')
    def assert_passwords_match(cls, password_confirmation, values):
        if not password_confirmation == values['password']:
            raise ValueError("Passwords do not match.")
        if len(password_confirmation) < 1:
            # TODO use more robust password rules
            raise ValueError("Password must not be empty.")
        return password_confirmation


class UserUpdate(AbstractUserInfoValidation):
    """Scheme for user updates.

    All fields here are optional, but passwords must match if at least one was
    provided.
    Note that even administrator updates can specify 'gender' and 'date_of_birth'
    fields, the function inserting the update into the db should handle this (and
    just ignore the fields).
    Switching user types is prohibited.
    """

    email: Optional[str]
    first_name: Optional[str]
    last_name: Optional[str]

    gender: Optional[Gender]
    date_of_birth: Optional[date]

    password: Optional[str] = None
    password_confirmation: Optional[str] = None

    @validator('password_confirmation')
    def assert_passwords_match_or_are_both_none(cls, password_confirmation, values):
        password = values.get('password')
        if None not in [password, password_confirmation]:
            if not password == password_confirmation:
                raise ValueError("Passwords do not match.")
            if len(password_confirmation) < 1:
                # TODO use more robust password rules
                raise ValueError("Password must not be empty.")
        return password_confirmation


class User(AbstractUser):
    """Final representation of all types of users, wrapped into one User schema.

    The id, created and updated fields are filled by the db during creation, so
    they are not needed in the parent classes.
    """

    id: int
    created: datetime
    updated: datetime
    devices: list[Device] = []

    class Config:
        orm_mode = True
