import typing
from typing import Union
from fastapi import status
from fastapi.requests import Request
from fastapi.exceptions import HTTPException

from api_objects import OccupantTokenObject, EmployeeTokenObject
from api_services.token_service import TokenService
from api_services.redis.functions import RedisActions
from api_library.response_handlers import ResponseHandler
from api_library.logger import user_logger
from api_configs import Auth, ApiStatic
from api_events.events.abstract_class import MethodToEvent, ActionsSchema

from databases import (
    Companies,
    Staff,
    Duties,
    Departments,
    Employees,
    BuildLivingSpace,
    BuildParts,
    Build,
    Duty,
    Event2Occupant,
    Event2Employee,
    Users,
    UsersTokens,
    OccupantTypes,
    RelationshipEmployee2Build,
)

from api_services import (
    send_email,
    save_access_token_to_redis,
    update_selected_to_redis,
    password_is_changed_template,
    change_your_password_template,
)

from api_validations.validations_request import (
    Login,
    Logout,
    ChangePassword,
    Remember,
    Forgot,
    CreatePassword,
    OccupantSelection,
    EmployeeSelection,
)

from api_validations.validations_response import (
    AuthenticationLoginResponse,
    AuthenticationRefreshResponse,
    AuthenticationUserInfoResponse,
)


class AuthenticationLoginEventMethods(MethodToEvent):
    event_type = "LOGIN"
    event_description = "Login via domain and access key : [email] | [phone]"
    event_category = "AUTHENTICATION"

    __event_keys__ = {
        "e672846d-cc45-4d97-85d5-6f96747fac67": "authentication_login_with_domain_and_creds",
    }
    __event_validation__ = {
        "e672846d-cc45-4d97-85d5-6f96747fac67": AuthenticationLoginResponse,
    }

    @classmethod
    def authentication_login_with_domain_and_creds(cls, data: Login, request: Request):
        try:
            access_dict = Users.login_user_with_credentials(data=data, request=request)
            found_user = access_dict.get("user")

            if not found_user:
                user_logger.log_login_attempt(
                    request,
                    None,
                    data.domain,
                    data.access_key,
                    success=False,
                    error="Invalid credentials",
                )
                return ResponseHandler.unauthorized("Invalid credentials")

            user_logger.log_login_attempt(
                request, found_user.id, data.domain, data.access_key, success=True
            )

            response_data = {
                "access_token": access_dict.get("access_token"),
                "refresh_token": access_dict.get("refresher_token"),
                "access_object": access_dict.get("access_object"),
                "user": found_user.get_dict(),
            }
            return ResponseHandler.success(
                message="User logged in successfully",
                data=response_data,
                response_model=AuthenticationLoginResponse,
            )
        except Exception as e:
            user_logger.log_login_attempt(
                request, None, data.domain, data.access_key, success=False, error=str(e)
            )
            raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=str(e))


class AuthenticationSelectEventMethods(MethodToEvent):
    event_type = "LOGIN"
    event_description = "Select Employee Duty or Occupant Type"
    event_category = "AUTHENTICATION"

    __event_keys__ = {
        "cee96b9b-8487-4e9f-aaed-2e8c79687bf9": "authentication_select_company_or_occupant_type",
    }
    __event_validation__ = {
        "cee96b9b-8487-4e9f-aaed-2e8c79687bf9": "authentication_select_company_or_occupant_type",
    }

    @classmethod
    def _handle_employee_selection(
        cls, data: EmployeeSelection, token_dict: EmployeeTokenObject, request: Request
    ):
        """Handle employee company selection"""
        if data.company_uu_id not in token_dict.companies_uu_id_list:
            return ResponseHandler.unauthorized(
                "Company not found in user's company list"
            )

        selected_company = Companies.filter_one(
            Companies.uu_id == data.company_uu_id
        ).data
        if not selected_company:
            return ResponseHandler.not_found("Company not found")

        # Get department IDs for the company
        department_ids = [
            dept.id
            for dept in Departments.filter_all(
                Departments.company_id == selected_company.id
            ).data
        ]

        # Get duties IDs for the company
        duties_ids = [
            duty.id
            for duty in Duties.filter_all(Duties.company_id == selected_company.id).data
        ]

        # Get staff IDs
        staff_ids = [
            staff.id for staff in Staff.filter_all(Staff.duties_id.in_(duties_ids)).data
        ]

        # Get employee
        employee = Employees.filter_one(
            Employees.people_id == token_dict.person_id,
            Employees.staff_id.in_(staff_ids),
        ).data

        if not employee:
            return ResponseHandler.not_found("Employee not found")

        # Get reachable events
        reachable_event_list_id = Event2Employee.get_event_id_by_employee_id(
            employee_id=employee.id
        )

        # Get staff and duties
        staff = Staff.filter_one(Staff.id == employee.staff_id).data
        duties = Duties.filter_one(Duties.id == staff.duties_id).data
        department = Departments.filter_one(Departments.id == duties.department_id).data

        # Get bulk duty
        bulk_id = Duty.filter_by_one(system=True, duty_code="BULK").data
        bulk_duty_id = Duties.filter_by_one(
            company_id=selected_company.id,
            duties_id=bulk_id.id,
            **Duties.valid_record_dict,
        ).data

        # Create company token
        company_token = CompanyToken(
            company_uu_id=selected_company.uu_id.__str__(),
            company_id=selected_company.id,
            department_id=department.id,
            department_uu_id=department.uu_id.__str__(),
            duty_id=duties.id,
            duty_uu_id=duties.uu_id.__str__(),
            bulk_duties_id=bulk_duty_id.id,
            staff_id=staff.id,
            staff_uu_id=staff.uu_id.__str__(),
            employee_id=employee.id,
            employee_uu_id=employee.uu_id.__str__(),
            reachable_event_list_id=reachable_event_list_id,
        )

        # Update Redis
        update_selected_to_redis(request=request, add_payload=company_token)
        return ResponseHandler.success("Company selected successfully")

    @classmethod
    def _handle_occupant_selection(
        cls, data: OccupantSelection, token_dict: OccupantTokenObject, request: Request
    ):
        """Handle occupant type selection"""
        # Get occupant type
        occupant_type = OccupantTypes.filter_by_one(
            system=True, uu_id=data.occupant_uu_id
        ).data
        if not occupant_type:
            return ResponseHandler.not_found("Occupant Type not found")

        # Get build part
        build_part = BuildParts.filter_by_one(
            system=True, uu_id=data.build_part_uu_id
        ).data
        if not build_part:
            return ResponseHandler.not_found("Build Part not found")

        # Get build and company info
        build = Build.filter_one(Build.id == build_part.build_id).data
        related_company = RelationshipEmployee2Build.filter_one(
            RelationshipEmployee2Build.member_id == build.id
        ).data
        company_related = Companies.filter_one(
            Companies.id == related_company.company_id
        ).data
        responsible_employee = Employees.filter_one(
            Employees.id == related_company.employee_id
        ).data

        # Get selected occupant type
        selected_occupant_type = BuildLivingSpace.filter_one(
            BuildLivingSpace.occupant_type == occupant_type.id,
            BuildLivingSpace.person_id == token_dict.person_id,
            BuildLivingSpace.build_parts_id == build_part.id,
        ).data
        if not selected_occupant_type:
            return ResponseHandler.not_found("Selected occupant type not found")

        # Get reachable events
        reachable_event_list_id = Event2Occupant.get_event_id_by_build_living_space_id(
            build_living_space_id=selected_occupant_type.id
        )

        # Create occupant token
        occupant_token = OccupantToken(
            living_space_id=selected_occupant_type.id,
            living_space_uu_id=selected_occupant_type.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_list_id=reachable_event_list_id,
        )

        # Update Redis
        update_selected_to_redis(request=request, add_payload=occupant_token)
        return ResponseHandler.success("Occupant selected successfully")

    @classmethod
    def authentication_select_company_or_occupant_type(
        cls,
        request: Request,
        data: Union[EmployeeSelection, OccupantSelection],
        token_dict: Union[EmployeeTokenObject, OccupantTokenObject],
    ):
        """Handle selection of company or occupant type"""
        try:
            if isinstance(token_dict, EmployeeTokenObject):
                return cls._handle_employee_selection(data, token_dict, request)
            elif isinstance(token_dict, OccupantTokenObject):
                return cls._handle_occupant_selection(data, token_dict, request)
            return ResponseHandler.error(
                "Invalid token type", status_code=status.HTTP_400_BAD_REQUEST
            )
        except Exception as e:
            return ResponseHandler.error(
                str(e), status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
            )


class AuthenticationCheckTokenEventMethods(MethodToEvent):
    event_type = "LOGIN"
    event_description = "Check Token is valid for user"
    event_category = "AUTHENTICATION"

    __event_keys__ = {
        "73d77e45-a33f-4f12-909e-3b56f00d8a12": "authentication_check_token_is_valid",
    }
    __event_validation__ = {
        "73d77e45-a33f-4f12-909e-3b56f00d8a12": "authentication_check_token_is_valid",
    }

    @classmethod
    def authentication_check_token_is_valid(cls, request: Request):
        try:
            TokenService.validate_token(request)
            return ResponseHandler.success("Access Token is valid")
        except HTTPException:
            return ResponseHandler.unauthorized("Access Token is NOT valid")


class AuthenticationRefreshEventMethods(MethodToEvent):
    event_type = "LOGIN"
    event_description = "Refresh user info using access token"
    event_category = "AUTHENTICATION"

    __event_keys__ = {
        "48379bb2-ba81-4d8e-a9dd-58837cfcbf67": "authentication_refresh_user_info",
    }
    __event_validation__ = {
        "48379bb2-ba81-4d8e-a9dd-58837cfcbf67": AuthenticationRefreshResponse,
    }

    @classmethod
    def authentication_refresh_user_info(
        cls,
        request: Request,
        token_dict: Union[EmployeeTokenObject, OccupantTokenObject],
    ):
        try:
            access_token = request.headers.get(Auth.ACCESS_TOKEN_TAG)
            if not access_token:
                return ResponseHandler.unauthorized()

            # Get user and token info
            found_user = Users.filter_one(Users.uu_id == token_dict.user_uu_id).data
            if not found_user:
                return ResponseHandler.not_found("User not found")

            user_token = UsersTokens.filter_one(
                UsersTokens.domain == found_user.domain_name,
                UsersTokens.user_id == found_user.id,
                UsersTokens.token_type == "RememberMe",
            ).data

            # Update user metadata
            TokenService.update_user_metadata(found_user, request)

            response_data = {
                "access_token": access_token,
                "refresh_token": getattr(user_token, "token", None),
                "user": found_user.get_dict(),
            }
            return ResponseHandler.success(
                "User info refreshed successfully",
                data=response_data,
                response_model=AuthenticationRefreshResponse,
            )
        except Exception as e:
            return ResponseHandler.error(str(e))


class AuthenticationChangePasswordEventMethods(MethodToEvent):
    event_type = "LOGIN"
    event_description = "Change password with access token"
    event_category = "AUTHENTICATION"

    __event_keys__ = {
        "f09f7c1a-bee6-4e32-8444-962ec8f39091": "authentication_change_password",
    }
    __event_validation__ = {
        "f09f7c1a-bee6-4e32-8444-962ec8f39091": "authentication_change_password",
    }

    @classmethod
    def authentication_change_password(
        cls,
        request: Request,
        data: ChangePassword,
        token_dict: Union[EmployeeTokenObject, OccupantTokenObject],
    ):
        try:
            if not isinstance(token_dict, EmployeeTokenObject):
                return ResponseHandler.unauthorized(
                    "Only employees can change password"
                )

            found_user = Users.filter_one(Users.uu_id == token_dict.user_uu_id).data
            if not found_user:
                return ResponseHandler.not_found("User not found")

            if not found_user.check_password(data.old_password):
                user_logger.log_password_change(
                    request,
                    found_user.id,
                    "change",
                    success=False,
                    error="Invalid old password",
                )
                return ResponseHandler.unauthorized("Old password is incorrect")

            found_user.set_password(data.new_password)
            user_logger.log_password_change(
                request, found_user.id, "change", success=True
            )

            return ResponseHandler.success("Password changed successfully")
        except Exception as e:
            user_logger.log_password_change(
                request,
                found_user.id if found_user else None,
                "change",
                success=False,
                error=str(e),
            )
            return ResponseHandler.error(str(e))


class AuthenticationCreatePasswordEventMethods(MethodToEvent):

    event_type = "LOGIN"
    event_description = "Create password with password reset token requested via email"
    event_category = "AUTHENTICATION"

    __event_keys__ = {
        "c519f9af-92e1-47b2-abf7-5a3316d075f7": "authentication_create_password",
    }
    __event_validation__ = {
        "c519f9af-92e1-47b2-abf7-5a3316d075f7": "authentication_create_password",
    }

    @classmethod
    def authentication_create_password(cls, data: CreatePassword):

        if not data.re_password == data.password:
            raise HTTPException(
                status_code=status.HTTP_406_NOT_ACCEPTABLE, detail="Password must match"
            )
        if found_user := Users.filter_one(
            Users.password_token == data.password_token
        ).data:
            found_user.create_password(found_user=found_user, password=data.password)
            found_user.password_token = ""
            found_user.save()
            send_email_completed = send_email(
                subject=f"Dear {found_user.user_tag}, your password has been changed.",
                receivers=[str(found_user.email)],
                html=password_is_changed_template(user_name=found_user.user_tag),
            )
            if not send_email_completed:
                raise HTTPException(
                    status_code=400, detail="Email can not be sent. Try again later"
                )
            return ResponseHandler.success(
                "Password is created successfully",
                data=found_user.get_dict(),
            )
        return ResponseHandler.not_found("Record not found")


class AuthenticationDisconnectUserEventMethods(MethodToEvent):

    event_type = "LOGIN"
    event_description = "Disconnect all sessions of user in access token"
    event_category = "AUTHENTICATION"

    __event_keys__ = {
        "8b586848-2fb3-4161-abbe-642157eec7ce": "authentication_disconnect_user",
    }
    __event_validation__ = {
        "8b586848-2fb3-4161-abbe-642157eec7ce": "authentication_disconnect_user",
    }

    @classmethod
    def authentication_disconnect_user(
        cls, data: Logout, token_dict: Union[EmployeeTokenObject, OccupantTokenObject]
    ):
        found_user = Users.filter_one(Users.uu_id == token_dict.user_uu_id).data
        if not found_user:
            return ResponseHandler.not_found("User not found")
        if already_tokens := RedisActions.get_object_via_user_uu_id(
            user_id=str(found_user.uu_id)
        ):
            for key, token_user in already_tokens.items():
                RedisActions.delete_key(key)
                selected_user = Users.filter_one(
                    Users.uu_id == token_user.get("uu_id"),
                ).data
                selected_user.remove_refresher_token(
                    domain=data.domain, disconnect=True
                )
                return ResponseHandler.success(
                    "All sessions are disconnected",
                    data=selected_user.get_dict(),
                )
        return ResponseHandler.not_found("Invalid data")


class AuthenticationLogoutEventMethods(MethodToEvent):

    event_type = "LOGIN"
    event_description = "Logout only single session of user which domain is provided"
    event_category = "AUTHENTICATION"

    __event_keys__ = {
        "5cc22e4e-a0f7-4077-be41-1871feb3dfd1": "authentication_logout_user",
    }
    __event_validation__ = {
        "5cc22e4e-a0f7-4077-be41-1871feb3dfd1": "authentication_logout_user",
    }

    @classmethod
    def authentication_logout_user(
        cls, request: Request, data: Logout, token_dict: dict = None
    ):
        token_user = None
        if already_tokens := RedisActions.get_object_via_access_key(request=request):
            for key in already_tokens:
                token_user = json.loads(RedisActions.get_key(key) or {})
                if token_user.get("domain") == data.domain:
                    RedisActions.delete_key(key)
                    selected_user = Users.filter_one(
                        Users.uu_id == token_user.get("uu_id"),
                    ).data
                    selected_user.remove_refresher_token(domain=data.domain)

            return ResponseHandler.success(
                "Session is logged out",
                data=token_user,
            )
        return ResponseHandler.not_found("Logout is not successfully completed")


class AuthenticationRefreshTokenEventMethods(MethodToEvent):

    event_type = "LOGIN"
    event_description = "Refresh access token with refresher token"
    event_category = "AUTHENTICATION"

    __event_keys__ = {
        "c90f3334-10c9-4181-b5ff-90d98a0287b2": "authentication_refresher_token",
    }
    __event_validation__ = {
        "c90f3334-10c9-4181-b5ff-90d98a0287b2": AuthenticationRefreshResponse,
    }

    @classmethod
    def authentication_refresher_token(
        cls, request: Request, data: Remember, token_dict: dict = None
    ):
        token_refresher = UsersTokens.filter_by_one(
            token=data.refresh_token,
            domain=data.domain,
            **UsersTokens.valid_record_dict,
        ).data
        if not token_refresher:
            return ResponseHandler.not_found("Invalid data")
        if found_user := Users.filter_one(
            Users.id == token_refresher.user_id,
        ).data:
            found_user: Users = found_user
            access_key = save_access_token_to_redis(
                request=request, found_user=found_user, domain=data.domain
            )
            found_user.last_agent = request.headers.get("User-Agent", None)
            found_user.last_platform = request.headers.get("Origin", None)
            found_user.last_remote_addr = getattr(
                request, "remote_addr", None
            ) or request.headers.get("X-Forwarded-For", None)
            found_user.last_seen = str(system_arrow.now())
            response_data = {
                "access_token": access_key,
                "refresh_token": data.refresh_token,
            }
            return ResponseHandler.success(
                "User is logged in successfully via refresher token",
                data=response_data,
                response_model=AuthenticationRefreshResponse,
            )
        return ResponseHandler.not_found("Invalid data")


class AuthenticationForgotPasswordEventMethods(MethodToEvent):

    event_type = "LOGIN"
    event_description = "Send an email to user for a valid password reset token"
    event_category = "AUTHENTICATION"

    __event_keys__ = {
        "e3ca6e24-b9f8-4127-949c-3bfa364e3513": "authentication_forgot_password",
    }
    __event_validation__ = {
        "e3ca6e24-b9f8-4127-949c-3bfa364e3513": "authentication_forgot_password",
    }

    @classmethod
    def authentication_forgot_password(
        cls,
        request: Request,
        data: Forgot,
    ):
        found_user: Users = Users.check_user_exits(
            access_key=data.access_key, domain=data.domain
        )
        forgot_key = save_access_token_to_redis(
            request=request, found_user=found_user, domain=data.domain
        )
        forgot_link = ApiStatic.forgot_link(forgot_key=forgot_key)
        send_email_completed = send_email(
            subject=f"Dear {found_user.user_tag}, your forgot password link has been sent.",
            receivers=[str(found_user.email)],
            html=change_your_password_template(
                user_name=found_user.user_tag, forgot_link=forgot_link
            ),
        )
        if not send_email_completed:
            raise HTTPException(
                status_code=400, detail="Email can not be sent. Try again later"
            )
        found_user.password_token = forgot_key
        found_user.password_token_is_valid = str(system_arrow.shift(days=1))
        found_user.save()

        return ResponseHandler.success(
            "Password is change link is sent to your email or phone",
            data={},
        )


class AuthenticationResetPasswordEventMethods(MethodToEvent):

    event_type = "UPDATE"
    __event_keys__ = {
        "af9e121e-24bb-44ac-a616-471d5754360e": "authentication_reset_password",
    }

    @classmethod
    def authentication_reset_password(cls, data: Forgot):
        from sqlalchemy import or_

        found_user = Users.query.filter(
            or_(
                Users.email == str(data.access_key).lower(),
                Users.phone_number == str(data.access_key).replace(" ", ""),
            ),
        ).first()
        if not found_user:
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail="Given access key or domain is not matching with the any user record.",
            )

        reset_password_token = found_user.reset_password_token(found_user=found_user)
        send_email_completed = send_email(
            subject=f"Dear {found_user.user_tag}, a password reset request has been received.",
            receivers=[str(found_user.email)],
            html=change_your_password_template(
                user_name=found_user.user_tag,
                forgot_link=ApiStatic.forgot_link(forgot_key=reset_password_token),
            ),
        )
        if not send_email_completed:
            raise found_user.raise_http_exception(
                status_code=400, message="Email can not be sent. Try again later"
            )
        return ResponseHandler.success(
            "Password change link is sent to your email or phone",
            data=found_user.get_dict(),
        )


class AuthenticationDownloadAvatarEventMethods(MethodToEvent):

    event_type = "LOGIN"
    event_description = "Download avatar icon and profile info of user"
    event_category = "AUTHENTICATION"

    __event_keys__ = {
        "c140cd5f-307f-4046-a93e-3ade032a57a7": "authentication_download_avatar",
    }
    __event_validation__ = {
        "c140cd5f-307f-4046-a93e-3ade032a57a7": AuthenticationUserInfoResponse,
    }

    @classmethod
    def authentication_download_avatar(
        cls, token_dict: Union[EmployeeTokenObject, OccupantTokenObject]
    ):
        if found_user := Users.filter_one(Users.id == token_dict.user_id).data:
            expired_starts = str(
                system_arrow.now() - system_arrow.get(str(found_user.expiry_ends))
            )
            expired_int = (
                system_arrow.now() - system_arrow.get(str(found_user.expiry_ends))
            ).days

            user_info = {
                "lang": token_dict.lang,
                "full_name": found_user.person.full_name,
                "avatar": found_user.avatar,
                "remember_me": found_user.remember_me,
                "expiry_ends": str(found_user.expiry_ends),
                "expired_str": expired_starts,
                "expired_int": int(expired_int),
            }
            return ResponseHandler.success(
                "Avatar and profile is shared via user credentials",
                data=user_info,
                response_model=AuthenticationUserInfoResponse,
            )
        return ResponseHandler.not_found("Invalid data")


AuthenticationLoginEventMethod = AuthenticationLoginEventMethods(
    action=ActionsSchema(endpoint="/authentication/login")
)
AuthenticationSelectEventMethod = AuthenticationSelectEventMethods(
    action=ActionsSchema(endpoint="/authentication/select")
)
AuthenticationCheckTokenEventMethod = AuthenticationCheckTokenEventMethods(
    action=ActionsSchema(endpoint="/authentication/valid")
)
AuthenticationRefreshEventMethod = AuthenticationRefreshEventMethods(
    action=ActionsSchema(endpoint="/authentication/refresh")
)
AuthenticationChangePasswordEventMethod = AuthenticationChangePasswordEventMethods(
    action=ActionsSchema(endpoint="/authentication/change_password")
)
AuthenticationCreatePasswordEventMethod = AuthenticationCreatePasswordEventMethods(
    action=ActionsSchema(endpoint="/authentication/create_password")
)
AuthenticationDisconnectUserEventMethod = AuthenticationDisconnectUserEventMethods(
    action=ActionsSchema(endpoint="/authentication/disconnect")
)
AuthenticationLogoutEventMethod = AuthenticationLogoutEventMethods(
    action=ActionsSchema(endpoint="/authentication/logout")
)
AuthenticationRefreshTokenEventMethod = AuthenticationRefreshTokenEventMethods(
    action=ActionsSchema(endpoint="/authentication/refresher")
)
AuthenticationForgotPasswordEventMethod = AuthenticationForgotPasswordEventMethods(
    action=ActionsSchema(endpoint="/authentication/forgot")
)
AuthenticationDownloadAvatarEventMethod = AuthenticationDownloadAvatarEventMethods(
    action=ActionsSchema(endpoint="/authentication/avatar")
)
AuthenticationResetPasswordEventMethod = AuthenticationResetPasswordEventMethods(
    action=ActionsSchema(endpoint="/authentication/reset_password")
)
