import arrow

from typing import Any, Dict, Optional, Union
from config import api_config
from schemas import (
    Users,
    People,
    BuildLivingSpace,
    BuildParts,
    OccupantTypes,
    Employees,
    Addresses,
    Companies,
    Staff,
    Duty,
    Duties,
    Departments,
    Event2Employee,
    Application2Occupant,
    Event2Occupant,
    Application2Employee,
    RelationshipEmployee2Build,
    Events,
    EndpointRestriction,
)
from api_modules.token.password_module import PasswordModule
from api_controllers.mongo.database import mongo_handler
from api_validations.token.validations import TokenDictType, EmployeeTokenObject, OccupantTokenObject, CompanyToken, OccupantToken, UserType
from api_validations.defaults.validations import CommonHeaders
from api_modules.redis.redis_handlers import RedisHandlers
from validations.password.validations import PasswordHistoryViaUser


class UserHandlers:

    @staticmethod
    def check_user_exists(access_key: str, db_session) -> Users:
        """Check if the user exists in the database."""
        Users.set_session(db_session)
        if "@" in access_key:
            found_user: Users = Users.query.filter(Users.email == access_key.lower()).first()
        else:
            found_user: Users = Users.query.filter(Users.phone_number == access_key.replace(" ", "")).first()
        if not found_user:
            raise ValueError("EYS_0003")
        return found_user

    @staticmethod
    def check_password_valid(domain: str, id_: str, password: str, password_hashed: str) -> bool:
        """Check if the password is valid."""
        if PasswordModule.check_password(domain=domain, id_=id_, password=password, password_hashed=password_hashed):
            return True
        raise ValueError("EYS_0004")

    @staticmethod
    def update_password():
        return


class LoginHandler:

    @staticmethod
    def is_occupant(email: str):
        return not str(email).split("@")[1] == api_config.ACCESS_EMAIL_EXT

    @staticmethod
    def is_employee(email: str):
        return str(email).split("@")[1] == api_config.ACCESS_EMAIL_EXT

    @classmethod
    # headers: CommonHeaders
    def do_employee_login(cls, headers: CommonHeaders, data: Any, db_session):
        """Handle employee login."""

        language = headers.request.headers.get("language", "tr")
        domain = headers.request.headers.get("domain", None)
        timezone = headers.request.headers.get("tz", None) or "GMT+3"

        user_handler, other_domains_list, main_domain = UserHandlers(), [], ""
        found_user = user_handler.check_user_exists(access_key=data.access_key, db_session=db_session)
        with mongo_handler.collection(f"{str(found_user.related_company)}*Domain") as collection:
            result = collection.find_one({"user_uu_id": str(found_user.uu_id)})
            if not result:
                raise ValueError("EYS_00087")
            other_domains_list = result.get("other_domains_list", [])
            main_domain = result.get("main_domain", None)
            if domain not in other_domains_list or not main_domain:
                raise ValueError("EYS_00088")

        if not user_handler.check_password_valid(domain=main_domain, id_=str(found_user.uu_id), password=data.password, password_hashed=found_user.hash_password):
            raise ValueError("EYS_0005")
        list_of_returns = (
            Employees.id, Employees.uu_id, People.id, People.uu_id, Users.id, Users.uu_id, Companies.id, Companies.uu_id,
            Departments.id, Departments.uu_id, Duty.id, Duty.uu_id, Companies.public_name, Companies.company_type, Duty.duty_name,
            Addresses.letter_address
        )

        list_employee_query = db_session.query(*list_of_returns
        ).join(Staff, Staff.id == Employees.staff_id
        ).join(People, People.id == Employees.people_id
        ).join(Duties, Duties.id == Staff.duties_id
        ).join(Duty, Duty.id == Duties.duties_id
        ).join(Departments, Departments.id == Duties.department_id
        ).join(Companies, Companies.id == Departments.company_id
        ).join(Users, Users.person_id == People.id
        ).outerjoin(Addresses, Addresses.id == Companies.official_address_id
        ).filter(Employees.people_id == found_user.person_id)
        list_employees, list_employees_query_all = [], list_employee_query.all()
        if not list_employees_query_all:
            ValueError("No Employee found for this user")

        for employee in list_employees_query_all:
            single_employee = {}
            for ix, returns in enumerate(list_of_returns):
                single_employee[str(returns)] = employee[ix]
            list_employees.append(single_employee)
        companies_uu_id_list, companies_id_list, companies_list, duty_uu_id_list, duty_id_list  = [], [], [], [], []
        for list_employee in list_employees:
            companies_id_list.append(int(list_employee["Companies.id"]))
            companies_uu_id_list.append(str(list_employee["Companies.uu_id"]))
            duty_uu_id_list.append(str(list_employee["Duty.uu_id"]))
            duty_id_list.append(int(list_employee["Duty.id"]))
            companies_list.append({
                "uu_id": str(list_employee["Employees.uu_id"]), 
                "public_name": list_employee["Companies.public_name"], 
                "company_type": list_employee["Companies.company_type"], 
                "company_address": list_employee["Addresses.letter_address"], 
                "duty": list_employee["Duty.duty_name"], 
                "company_uuid": str(list_employee["Companies.uu_id"])
            })
        model_value = EmployeeTokenObject(
            user_type=UserType.employee.value, user_uu_id=str(found_user.uu_id), user_id=found_user.id, person_id=found_user.person_id,
            person_uu_id=str(list_employees[0]["People.uu_id"]), request=dict(headers.request.headers), domain_list=other_domains_list,
            companies_uu_id_list=companies_uu_id_list, companies_id_list=companies_id_list, duty_uu_id_list=duty_uu_id_list, duty_id_list=duty_id_list,
        ).model_dump()
        employee_uuid = str(companies_list[0]["uu_id"])
        print('employee_uuid', employee_uuid)
        set_to_redis_dict = dict(
            user=found_user, token=model_value, add_uuid=employee_uuid, 
            header_info=dict(language=headers.language, domain=headers.domain, timezone=headers.timezone),
        )
        user_dict = found_user.get_dict()
        person_dict = found_user.person.get_dict()
        if access_token := RedisHandlers().set_object_to_redis(**set_to_redis_dict):
            return {
                "access_token": access_token,
                "user_type": UserType.employee.name,
                "user": {
                    "uuid": user_dict["uu_id"], "avatar": user_dict["avatar"], "email": user_dict["email"], "phone_number": user_dict["phone_number"], "user_tag": user_dict["user_tag"],
                    "password_expiry_begins": str(arrow.get(user_dict["password_expiry_begins"]).shift(days=int(user_dict["password_expires_day"]))),
                    "person": {
                        "uuid": person_dict["uu_id"], "firstname": person_dict["firstname"], "surname": person_dict["surname"],
                        "middle_name": person_dict["middle_name"], "sex_code": person_dict["sex_code"], "person_tag": person_dict["person_tag"],
                        "country_code": person_dict["country_code"], "birth_date": person_dict["birth_date"],
                    },
                },
                "selection_list": companies_list,
            }
        raise ValueError("Something went wrong")

    @classmethod
    # headers=headers, data=data, db_session=db_session
    def do_occupant_login(cls, headers: CommonHeaders, data: Any, db_session):
        """
        Handle occupant login.
        """
        language = headers.request.headers.get("language", "tr")
        domain = headers.request.headers.get("domain", None)
        timezone = headers.request.headers.get("tz", None) or "GMT+3"
        BuildParts.set_session(db_session)
        OccupantTypes.set_session(db_session)

        user_handler = UserHandlers()
        found_user = user_handler.check_user_exists(access_key=data.access_key, db_session=db_session)
        other_domains_list, main_domain = [], ""
        with mongo_handler.collection(f"{str(found_user.related_company)}*Domain") as collection:
            result = collection.find_one({"user_uu_id": str(found_user.uu_id)})
            if not result:
                raise ValueError("EYS_00087")
            other_domains_list = result.get("other_domains_list", [])
            main_domain = result.get("main_domain", None)
            if domain not in other_domains_list or not main_domain:
                raise ValueError("EYS_00088")

        if not user_handler.check_password_valid(domain=main_domain, id_=str(found_user.uu_id), password=data.password, password_hashed=found_user.hash_password):
            raise ValueError("EYS_0005")

        occupants_selection_dict: Dict[str, Any] = {}
        living_spaces: list[BuildLivingSpace] = BuildLivingSpace.query.filter(BuildLivingSpace.person_id == found_user.person_id).all()

        if not living_spaces:
            raise ValueError("EYS_0006")
        for living_space in living_spaces:
            build_part = BuildParts.query.filter(BuildParts.id == living_space.build_parts_id).first()
            if not build_part:
                raise ValueError("EYS_0007")

            build = build_part.buildings
            occupant_type = OccupantTypes.query.filter(OccupantTypes.id == living_space.occupant_type_id).first()
            occupant_data = {
                "build_living_space_uu_id": str(living_space.uu_id), "part_uu_id": str(build_part.uu_id), "part_name": build_part.part_name(), "part_level": build_part.part_level,
                "occupant_uu_id": str(occupant_type.uu_id), "description": occupant_type.occupant_description, "code": occupant_type.occupant_code,
            }

            build_key = str(build.uu_id)
            if build_key not in occupants_selection_dict:
                occupants_selection_dict[build_key] = {"build_uu_id": build_key, "build_name": build.build_name, "build_no": build.build_no, "occupants": [occupant_data],}
            else:
                occupants_selection_dict[build_key]["occupants"].append(occupant_data)

            person = found_user.person
            model_value = OccupantTokenObject(
                user_type=UserType.occupant.value, user_uu_id=str(found_user.uu_id), user_id=found_user.id, person_id=person.id,
                person_uu_id=str(person.uu_id), domain_list=other_domains_list, request=dict(request.headers), available_occupants=occupants_selection_dict,
            ).model_dump()
            redis_handler = RedisHandlers()
            if access_token := redis_handler.set_object_to_redis(
                user=found_user, token=model_value, add_uuid=living_space.uu_id, 
                header_info=dict(language=language, domain=domain, timezone=timezone)
            ):
                return {"access_token": access_token, "user_type": UserType.occupant.name, "selection_list": occupants_selection_dict}
            raise ValueError("Something went wrong")

    @classmethod
    def authentication_login_with_domain_and_creds(cls, headers: CommonHeaders, data: Any):
        """
        Authenticate user with domain and credentials.

        Args:
            headers: CommonHeaders object
            data: Request body containing login credentials
        {
            "access_key": "karatay.berkay.sup@evyos.com.tr",
            "password": "string",
            "remember_me": false
        }
        Returns:
            SuccessResponse containing authentication token and user info
        """

        with Users.new_session() as db_session:
            if cls.is_employee(data.access_key):
                return cls.do_employee_login(headers=headers, data=data, db_session=db_session)
            elif cls.is_occupant(data.access_key):
                return cls.do_occupant_login(headers=headers, data=data, db_session=db_session)
            else:
                raise ValueError("Invalid email format")

    @classmethod
    def raise_error_if_request_has_no_token(cls, request: Any) -> None:
        """Validate request has required token headers."""
        if not hasattr(request, "headers"):
            raise ValueError("Request has no headers")
        if not request.headers.get(api_config.ACCESS_TOKEN_TAG):
            raise ValueError("Request has no access token")

    @classmethod
    def get_access_token_from_request(cls, request: Any) -> str:
        """Extract access token from request headers."""
        cls.raise_error_if_request_has_no_token(request=request)
        return request.headers.get(api_config.ACCESS_TOKEN_TAG)

    @classmethod
    def handle_employee_selection(cls, access_token: str, data: Any, token_dict: TokenDictType):
        with Users.new_session() as db_session:
            if data.uuid not in token_dict.companies_uu_id_list:
                ValueError("EYS_0011")
            list_of_returns = (
                Employees.id, Employees.uu_id, People.id, People.uu_id, Users.id, Users.uu_id, Companies.id, Companies.uu_id,
                Departments.id, Departments.uu_id, Duty.id, Duty.uu_id, Addresses.id, Addresses.letter_address, Staff.id, Staff.uu_id,
                Duties.id, Duties.uu_id, 
            )

            selected_company_query = db_session.query(*list_of_returns
            ).join(Staff, Staff.id == Employees.staff_id
            ).join(People, People.id == Employees.people_id
            ).join(Duties, Duties.id == Staff.duties_id
            ).join(Duty, Duty.id == Duties.duties_id
            ).join(Departments, Departments.id == Duties.department_id
            ).join(Companies, Companies.id == Departments.company_id
            ).join(Users, Users.person_id == People.id
            ).outerjoin(Addresses, Addresses.id == Companies.official_address_id
            ).filter(Employees.uu_id == data.uuid, Users.id == token_dict.user_id)

            selected_company_first = selected_company_query.first()
            if not selected_company_first:
                ValueError("Selected company not found")
            
            result_with_keys_dict = {}
            for ix, selected_company_item in enumerate(selected_company_first):
                result_with_keys_dict[str(list_of_returns[ix])] = selected_company_item

            if not selected_company_first:
                ValueError("EYS_0010")

            # Get reachable events
            # reachable_event_codes = Event2Employee.get_event_codes(employee_id=int(result_with_keys_dict['Employees.id']), db=db_session)
            # TODO: erase this bypass later
            filter_endpoints_and_events = db_session.query(EndpointRestriction.operation_uu_id, Events.function_code
            ).join(EndpointRestriction, EndpointRestriction.id == Events.endpoint_id).filter().all()
            reachable_event_codes = {endpoint_name: function_code for endpoint_name, function_code in filter_endpoints_and_events}
            # Get reachable applications
            reachable_app_codes = Application2Employee.get_application_codes(employee_id=int(result_with_keys_dict['Employees.id']), db=db_session)

            company_token = CompanyToken(
                company_uu_id=str(result_with_keys_dict['Companies.uu_id']),
                company_id=int(result_with_keys_dict['Companies.id']),
                department_id=int(result_with_keys_dict['Departments.id']),
                department_uu_id=str(result_with_keys_dict['Departments.uu_id']),
                duty_id=int(result_with_keys_dict['Duty.id']),
                duty_uu_id=str(result_with_keys_dict['Duty.uu_id']),
                bulk_duties_id=int(result_with_keys_dict['Duties.id']),
                staff_id=int(result_with_keys_dict['Staff.id']),
                staff_uu_id=str(result_with_keys_dict['Staff.uu_id']),
                employee_id=int(result_with_keys_dict['Employees.id']),
                employee_uu_id=str(result_with_keys_dict['Employees.uu_id']),
                reachable_event_codes=reachable_event_codes,
                reachable_app_codes=reachable_app_codes,
            )
            redis_handler = RedisHandlers()
            redis_result = redis_handler.update_token_at_redis(
                token=access_token, add_payload=company_token, add_uuid=str(result_with_keys_dict['Employees.uu_id'])
            )
            return {"selected_uu_id": data.uuid}

    @classmethod
    def handle_occupant_selection(cls, access_token: str, data: Any, token_dict: TokenDictType):
        """Handle occupant type selection"""
        with BuildLivingSpace.new_session() as db:
            # Get selected occupant type
            selected_build_living_space: BuildLivingSpace = BuildLivingSpace.filter_one(BuildLivingSpace.uu_id == data.build_living_space_uu_id, db=db).data
            if not selected_build_living_space:
                raise ValueError("EYS_0012")

            # Get reachable events
            reachable_event_codes = Event2Occupant.get_event_codes(build_living_space_id=selected_build_living_space.id, db=db)
            occupant_type = OccupantTypes.filter_one_system(OccupantTypes.id == selected_build_living_space.occupant_type_id, db=db).data
            build_part = BuildParts.filter_one(BuildParts.id == selected_build_living_space.build_parts_id, db=db)
            build = build_part.buildings
            reachable_app_codes = Application2Occupant.get_application_codes(build_living_space_id=selected_build_living_space.id, db=db)
            # responsible_employee = Employees.filter_one(
            #     Employees.id == build_part.responsible_employee_id,
            #     db=db,
            # ).data
            # related_company = RelationshipEmployee2Build.filter_one(
            #     RelationshipEmployee2Build.member_id == build.id,
            #     db=db,
            # ).data
            # Get company
            # company_related = Companies.filter_one(
            #     Companies.id == related_company.company_id,
            #     db=db,
            # ).data

            # Create occupant token
            occupant_token = OccupantToken(
                living_space_id=selected_build_living_space.id,
                living_space_uu_id=selected_build_living_space.uu_id.__str__(),
                occupant_type_id=occupant_type.id,
                occupant_type_uu_id=occupant_type.uu_id.__str__(),
                occupant_type=occupant_type.occupant_type,
                build_id=build.id,
                build_uuid=build.uu_id.__str__(),
                build_part_id=build_part.id,
                build_part_uuid=build_part.uu_id.__str__(),
                # responsible_employee_id=responsible_employee.id,
                # responsible_employee_uuid=responsible_employee.uu_id.__str__(),
                # responsible_company_id=company_related.id,
                # responsible_company_uuid=company_related.uu_id.__str__(),
                reachable_event_codes=reachable_event_codes,
                reachable_app_codes=reachable_app_codes,
            )
            redis_handler = RedisHandlers()
            redis_handler.update_token_at_redis(
                token=access_token, add_payload=occupant_token, add_uuid=occupant_token.living_space_uu_id
            )
            return {"selected_uu_id": occupant_token.living_space_uu_id}

    @classmethod  # Requires auth context
    def authentication_select_company_or_occupant_type(cls, request: Any, data: Any):
        """
        Handle selection of company or occupant type
        {"data": {"build_living_space_uu_id": ""}} | {"data": {"company_uu_id": ""}}
        {
          "data": {"company_uu_id": "e9869a25-ba4d-49dc-bb0d-8286343b184b"}
        }
        {
          "data": {"build_living_space_uu_id": "e9869a25-ba4d-49dc-bb0d-8286343b184b"}
        }
        """
        access_token = request.headers.get(api_config.ACCESS_TOKEN_TAG, None)
        if not access_token:
            raise ValueError("EYS_0001")

        token_object = RedisHandlers.get_object_from_redis(access_token=access_token)
        if token_object.is_employee:
            return cls.handle_employee_selection(access_token=access_token, data=data, token_dict=token_object)
        elif token_object.is_occupant:
            return cls.handle_occupant_selection(access_token=access_token, data=data, token_dict=token_object)

    @classmethod
    def authentication_check_token_valid(cls, domain, access_token: str) -> bool:
        redis_handler = RedisHandlers()
        if auth_token := redis_handler.get_object_from_redis(access_token=access_token):
            return True
        return False


class PasswordHandler:

    @staticmethod
    def create_password(password, password_token=None):
        with Users.new_session() as db_session:
            Users.set_session(db_session)
            found_user = Users.query.filter(Users.password_token == password_token).first()
            if not found_user:
                raise ValueError("EYS_0031")

            if found_user.password_token:
                replace_day = 0
                try:
                    replace_day = int(str(found_user.password_expires_day or 0).split(",")[0].replace(" days", ""))
                except Exception as e:
                    err = e
                token_is_expired = arrow.now() >= arrow.get(str(found_user.password_expiry_begins)).shift(days=replace_day)
                if not str(password_token) == str(found_user.password_token) or token_is_expired:
                    raise ValueError("EYS_0032")

            collection_name = f"{found_user.related_company}*Domain"
            with mongo_handler.collection(collection_name) as mongo_engine:
                domain_via_user = mongo_engine.find_one({"user_uu_id": str(found_user.uu_id)})
                if not domain_via_user:
                    raise ValueError("EYS_0024")
                domain_via_user = domain_via_user.get("main_domain", None)
                new_password_dict = {
                    "password": PasswordModule.create_hashed_password(domain=domain_via_user, id_=str(found_user.uu_id), password=password),
                    "date": str(arrow.now().date()),
                }
                history_dict = PasswordHistoryViaUser(user_uu_id=str(found_user.uu_id), password_add=new_password_dict, access_history_detail={"request": "", "ip": ""})
                found_user.password_expiry_begins = str(arrow.now())
                found_user.hash_password = new_password_dict.get("password")
                found_user.password_token = "" if found_user.password_token else ""
            
            collection_name = f"{found_user.related_company}*PasswordHistory"
            with mongo_handler.collection(collection_name) as mongo_engine_sc:
                password_history_item = mongo_engine_sc.find_one({"user_uu_id": str(found_user.uu_id)})
                if not password_history_item:
                    mongo_engine_sc.insert_one(document={"user_uu_id": str(found_user.uu_id), "password_history": []})
                password_history_item = mongo_engine_sc.find_one({"user_uu_id": str(found_user.uu_id)})
                password_history_list = password_history_item.get("password_history", [])
                hashed_password = history_dict.password_add.get("password")
                for password_in_history in password_history_list:
                    print('password_history_list', password_history_list, password_in_history)
                    if str(password_in_history.get("password")) == str(hashed_password):
                        raise ValueError("EYS_0032")
                if len(password_history_list) > 3:
                    password_history_list.pop(0)
                password_history_list.append(history_dict.password_add)
                return mongo_engine_sc.update_one(
                    filter={"user_uu_id": str(found_user.uu_id)},
                    update={"$set": {
                        "password_history": password_history_list, "modified_at": arrow.now().timestamp(), "access_history_detail": history_dict.access_history_detail
                    }}, upsert=True,
                )
                found_user.save(db=db_session)
                return found_user


class AuthHandlers:

    LoginHandler: LoginHandler = LoginHandler()
    PasswordHandler: PasswordHandler = PasswordHandler()
