from template_project.application.access_token.cryptographer import AccessTokenCryptographer
from template_project.application.access_token.entity_factory import AccessTokenFactory
from template_project.application.auth_identity.data_gateway import AuthIdentityDataGateway
from template_project.application.auth_identity.entity import AuthIdentity, AuthMethod
from template_project.application.auth_identity.errors import AuthError, InvalidCodeError, UserAlreadyExistsError
from template_project.application.common.containers import SecretString
from template_project.application.common.data_structure import to_data_structure
from template_project.application.common.interactor import to_interactor
from template_project.application.common.oauth.yandex import (
    OAuthClient as YandexOAuthClient,
    OAuthExchangeCodeError,
    OAuthLoadUserInfoError,
)
from template_project.application.common.unit_of_work import UnitOfWork
from template_project.application.user.entity import User, UserId
from template_project.application.user.password_utils import PasswordHasher
from template_project.application.user.profile.entity import Profile


@to_data_structure
class UserSignUpResponse:
    user_id: UserId
    access_token: str


@to_interactor
class SignUpInteractor:
    unit_of_work: UnitOfWork
    password_hasher: PasswordHasher
    auth_identity_data_gateway: AuthIdentityDataGateway
    access_token_factory: AccessTokenFactory
    access_token_cryptographer: AccessTokenCryptographer
    yandex_oauth_client: YandexOAuthClient

    async def sign_up_email(
        self,
        email: str,
        password: SecretString,
    ) -> UserSignUpResponse:
        existing_auth_identity = await self.auth_identity_data_gateway.load_by_method_and_identifier(
            method=AuthMethod.EMAIL,
            identifier=email,
        )

        if existing_auth_identity:
            raise UserAlreadyExistsError(identifier=email, auth_method=AuthMethod.EMAIL)

        hashed_password = self.password_hasher.hash(password)

        user = User.factory()

        auth_identity = AuthIdentity.factory(
            user_id=user.id,
            method=AuthMethod.EMAIL,
            identifier=email,
            secret_key=hashed_password,
        )

        access_token = self.access_token_factory.execute(user.id)
        crypted_access_token = self.access_token_cryptographer.crypto(access_token.id)

        profile = Profile.factory(user_id=user.id, email=email)

        for entity in (user, auth_identity, access_token, profile):
            await self.unit_of_work.add(entity)

        await self.unit_of_work.commit()

        return UserSignUpResponse(user_id=user.id, access_token=crypted_access_token)

    async def sign_up_yandex(self, code: str) -> UserSignUpResponse:
        try:
            token_response = await self.yandex_oauth_client.exchange_code(code)
            access_token_yandex = token_response.access_token

        except OAuthExchangeCodeError as error:
            raise InvalidCodeError from error

        try:
            user_info = await self.yandex_oauth_client.load_user_info(access_token_yandex)

        except OAuthLoadUserInfoError as error:
            raise AuthError from error

        existing_auth_identity = await self.auth_identity_data_gateway.load_by_method_and_identifier(
            method=AuthMethod.YANDEX,
            identifier=user_info.user_id,
        )

        if existing_auth_identity:
            user_id = existing_auth_identity.user_id

        else:
            user = User.factory()
            user_id = user.id

            auth_identity = AuthIdentity.factory(
                user_id=user_id,
                method=AuthMethod.YANDEX,
                identifier=user_info.user_id,
                secret_key=None,
            )

            profile = Profile.factory(user_id=user_id)

            await self.unit_of_work.add(user)
            await self.unit_of_work.add(auth_identity)
            await self.unit_of_work.add(profile)

        access_token = self.access_token_factory.execute(user_id)
        crypted_access_token = self.access_token_cryptographer.crypto(access_token.id)

        await self.unit_of_work.add(access_token)
        await self.unit_of_work.commit()

        return UserSignUpResponse(user_id=user_id, access_token=crypted_access_token)
