import datetime
import logging
from fastapi import Request
from fastapi.responses import JSONResponse
from starlette.middleware.base import BaseHTTPMiddleware
from typing import Any, TypeVar

from intranet.trip.src.api.auth import get_user_by_request, get_tvm_client
from intranet.trip.src.api.auth.tvm import SERVICE_TICKET_HEADER
from intranet.trip.src.config import settings
from intranet.trip.src.lib.utils import safe_getitem
from intranet.trip.src.models import User, UserUid, Company
from intranet.trip.src.middlewares.exceptions import (
    BaseAuthException,
    DeactivatedException,
    RejectedException,
    EmptyCompanyException,
    ForbiddenException,
    NotConfirmedEmailException,
    NotRegisteredException,
)
from intranet.trip.src.middlewares.utils import get_user_by_uid


logger = logging.getLogger(__name__)


UserBase = TypeVar('UserBase', bound=UserUid)


class YaTeamMiddleware(BaseHTTPMiddleware):
    """
    Authentication on yandex-team.ru
    """
    @staticmethod
    def is_without_authentication(request: Request) -> bool:
        """
        Check if authentication is not required
        :param request:
        :return: True if not required
        """
        # Если пришли за openapi для тестинга или стендов
        if settings.ENV_TYPE != 'production' and request.scope.get('path') == settings.OPENAPI_URL:
            return True
        # Если пришли за unistat ручкой
        if request.scope.get('path') in [settings.UNISTAT_URL, settings.EXTERNAL_UNISTAT_URL]:
            return True
        return False

    @staticmethod
    async def get_user_data_from_bb(request: Request) -> dict[str, Any]:
        """
        Get user from blackbox
        :param request:
        :return:
        """
        return await get_user_by_request(request)

    @staticmethod
    def is_deactivated(user: User) -> bool:
        """
        Return True if user is deactivated - access must be blocked
        We have role 'КПБ': (not user.is_active and user.is_limited_access)
        """
        return (
            user.is_dismissed
            or (not user.is_active and not user.is_coordinator and not user.is_limited_access)
        )

    @staticmethod
    def is_rejected(user: User) -> bool:
        """
        Return True if user was denied activation
        """
        return user.rejected_at is not None

    @staticmethod
    async def get_user_or_error(request: Request, blackbox_user: dict[str, Any]) -> UserBase:
        """
        Get user from trip DB or generate error
        :param request:
        :param blackbox_user:
        :return:
        """
        uid = blackbox_user['uid']['value']
        user = await get_user_by_uid(request, uid)
        if user is None or YaTeamMiddleware.is_deactivated(user):
            logger.warning('Not found uid: %s, alias: %s', uid, blackbox_user.get('aliases'))
            raise ForbiddenException
        user.user_ticket = blackbox_user.get('user_ticket')
        user.language = blackbox_user['attributes'].get(settings.BB_ATTRIBUTE_LANGUAGE)
        user.timezone = blackbox_user['attributes'].get(settings.BB_ATTRIBUTE_TIMEZONE)
        user.is_yandex_employee = True  # always True for Ya-team
        return user

    async def dispatch(self, request: Request, call_next):
        request.state.service_ticket = None
        request.state.user = None

        # Если пришли с сервисным TVM-тикетом
        raw_service_ticket = request.headers.get(SERVICE_TICKET_HEADER)
        if raw_service_ticket:
            service_ticket = await get_tvm_client().parse_service_ticket(raw_service_ticket)
            if not service_ticket:
                return JSONResponse(
                    status_code=401,
                    content={'detail': 'NOT_VALID_TICKET'},
                )

            request.state.service_ticket = service_ticket
            return await call_next(request)

        # Если не нужна авторизация
        if self.is_without_authentication(request):
            return await call_next(request)

        # Если пришли с кукой или oauth-токеном
        blackbox_user = await self.get_user_data_from_bb(request)
        status = blackbox_user.get('status')
        if not status or status['id'] != 0:
            logger.warning('User auth: FAILED to %s', request.url)
            return JSONResponse(
                status_code=401,
                content={'detail': 'UNAUTHORIZED'},
            )

        # Если пришли за токеном - не нужно ходить в БД
        if request.url.path == settings.TOKEN_URL:
            request.state.user = UserUid(uid=blackbox_user['uid']['value'])
            return await call_next(request)

        try:
            request.state.user = await self.get_user_or_error(request, blackbox_user)
        except BaseAuthException as e:
            logger.info('For user %s got status %s', blackbox_user['uid']['value'], str(e))
            return e.json_response

        return await call_next(request)


class DevMiddleware(YaTeamMiddleware):
    """
    Authentication for pytest (development)
    """
    @staticmethod
    async def get_user_data_from_bb(request: Request) -> dict[str, Any]:
        return {
            'error': 'OK',
            'uid': {'value': '1120000000000000', 'lite': False, 'hosted': False},
            'login': 'dev',
            'user_ticket': 'user:ticket',
            'attributes': {
                settings.BB_ATTRIBUTE_TIMEZONE: 'UTC',
                settings.BB_ATTRIBUTE_LANGUAGE: 'ru',
            },
            'is_coordinator': False,
            'aliases': {'1': 'dev'},
            'status': {
                'id': 0,
                'value': 'VALID',
            },
        }

    @staticmethod
    async def get_user_or_error(request: Request, blackbox_user: dict[str, Any]) -> UserBase:
        user = User(
            uid=blackbox_user['uid']['value'],
            person_id=1,
            login=blackbox_user['login'],
            timezone=blackbox_user['attributes'].get(settings.BB_ATTRIBUTE_TIMEZONE),
            language=blackbox_user['attributes'].get(settings.BB_ATTRIBUTE_LANGUAGE),
            user_ticket=blackbox_user['user_ticket'],
            first_name='Test',
            last_name='Test',
            is_dismissed=False,
            is_active=True,
            is_coordinator=blackbox_user['is_coordinator'],
            is_yandex_employee=settings.IS_YA_TEAM or '13' in blackbox_user['aliases'],
            email_confirmed_at=datetime.datetime.now(),
        )
        user.company = Company(company_id=1, name='Test company', holding_id=1)
        return user


class YandexMiddleware(YaTeamMiddleware):
    """
    Authentication on yandex.ru
    """
    @staticmethod
    async def get_user_or_error(request: Request, blackbox_user: dict[str, Any]) -> UserBase:
        uid = blackbox_user['uid']['value']
        user = await get_user_by_uid(request, uid)

        # пришли в ручку создания человека в трипе - дорегистрация
        if request.scope.get('path') == settings.CREATE_PERSON_URL:
            if user is None:
                return UserUid(uid=uid)  # переходим к созданию person
            logger.warning('uid: %s re-registration attempt', uid)
            raise ForbiddenException

        # пользователь не был получен - нужна дорегистрация
        if user is None:
            raise NotRegisteredException

        # пользователю отказано в активации
        if YandexMiddleware.is_rejected(user):
            raise RejectedException

        # пользователь не активен или уволен
        if YandexMiddleware.is_deactivated(user):
            raise DeactivatedException

        # получаем дополнительные атрибуты для user
        # https://docs.yandex-team.ru/authdevguide/concepts/DB_About#aliases
        user.is_yandex_employee = '13' in blackbox_user['aliases']
        user.user_ticket = blackbox_user.get('user_ticket')
        user.language = blackbox_user['attributes'].get(settings.BB_ATTRIBUTE_LANGUAGE)
        user.timezone = blackbox_user['attributes'].get(settings.BB_ATTRIBUTE_TIMEZONE)
        user.first_name = (
            user.first_name
            or blackbox_user['attributes'].get(settings.BB_ATTRIBUTE_FIRSTNAME)
        )
        user.last_name = (
            user.last_name
            or blackbox_user['attributes'].get(settings.BB_ATTRIBUTE_LASTNAME)
        )
        if not safe_getitem(blackbox_user, ['display_name', 'avatar', 'empty'], True):
            user.avatar_id = safe_getitem(blackbox_user, ['display_name', 'avatar', 'default'])

        # пришли в ручку подтверждения почты или привязки компании
        if request.scope.get('path') in (settings.CONFIRM_EMAIL_URL, settings.SET_COMPANY_URL):
            return user

        # почта не подтверждена
        if user.email_confirmed_at is None:
            raise NotConfirmedEmailException

        # компания не привязана
        if user.company_id is None:
            raise EmptyCompanyException

        return user


def get_auth_middleware_class():
    if not settings.IS_YA_TEAM:
        return YandexMiddleware
    if settings.ENV_TYPE == 'development':
        return DevMiddleware
    return YaTeamMiddleware
