"""AuthenticateUser — the ``POST /auth/login`` orchestration, framework-free.

Behavior mirrors ``auth_api.login`` exactly so the cut-over is invisible to clients:
institutional-email normalization → rate limit (429) → credential check (401) →
email-verified check (403) → role reconcile → audit → signed token.

Depends only on ports + domain; unit-tested with fakes (no DB). Status mapping
(DomainError subclass → HTTP code) happens in the API layer.
"""

from __future__ import annotations

from src.application.identity.dto import AuthenticatedUser, LoginCommand
from src.application.identity.ports import (
    AuthAuditSink,
    LoginRateLimiter,
    PasswordHasher,
    TokenIssuer,
)
from src.domain.identity.errors import EmailNotVerified, InvalidCredentials
from src.domain.identity.repository import UserRepository
from src.domain.identity.value_objects import InstitutionalEmail
from src.shared_kernel.errors import RateLimited

# Vietnamese messages preserved verbatim from auth_api.login.
_INVALID_CREDENTIALS_MSG = "Email hoặc mật khẩu không đúng."
_EMAIL_UNVERIFIED_MSG = (
    "Vui lòng xác minh email trước khi đăng nhập. Kiểm tra hộp thư "
    "hoặc dùng chức năng gửi lại mã OTP trên trang đăng ký."
)
_RATE_LIMITED_MSG = "Quá nhiều lần đăng nhập. Vui lòng thử lại sau."


class AuthenticateUser:
    def __init__(
        self,
        *,
        users: UserRepository,
        hasher: PasswordHasher,
        tokens: TokenIssuer,
        rate_limiter: LoginRateLimiter,
        audit: AuthAuditSink,
    ) -> None:
        self._users = users
        self._hasher = hasher
        self._tokens = tokens
        self._rate_limiter = rate_limiter
        self._audit = audit

    async def execute(self, command: LoginCommand) -> AuthenticatedUser:
        email = InstitutionalEmail.parse(command.email)  # raises 400 on bad domain

        if not self._rate_limiter.allow(email.value, command.client_ip):
            raise RateLimited(_RATE_LIMITED_MSG)

        user = await self._users.get_by_email(email.value)
        # Wrong creds and unknown/inactive email are indistinguishable → 401.
        if (
            user is None
            or not user.can_authenticate()
            or not self._hasher.verify(command.password, user.password_hash)
        ):
            await self._audit.login_failed(
                email=email.value,
                user_id=user.id if user is not None else None,
                reason=None,
            )
            raise InvalidCredentials(_INVALID_CREDENTIALS_MSG)

        # Correct creds but unverified email → 403 (distinct from 401).
        if user.requires_email_verification():
            await self._audit.login_failed(
                email=email.value, user_id=user.id, reason="email_unverified"
            )
            raise EmailNotVerified(_EMAIL_UNVERIFIED_MSG)

        roles = await self._users.roles_after_reconcile(user)
        await self._audit.login_succeeded(user_id=user.id, email=user.email, roles=roles)
        token = self._tokens.issue(user.id, user.email, roles, user.credential_version)
        return AuthenticatedUser(user=user, roles=roles, access_token=token)
