import hmac
import re
import uuid
from dataclasses import dataclass
from datetime import UTC, datetime, timedelta
from typing import Annotated
from urllib.parse import quote, urlencode

from fastapi import APIRouter, Cookie, Depends, Header, Request, Response
from pydantic import BaseModel, Field, field_validator
from redis.exceptions import RedisError
from sqlalchemy import and_, delete, func, select, update
from sqlalchemy.ext.asyncio import AsyncSession

from .audit import compute_audit_hash
from .authorization import permissions_for
from .catalog import PERMISSIONS, ROLE_PERMISSIONS
from .computer_catalog import DEFAULT_COMPUTER_TEMPLATE_KEY
from .config import get_settings
from .database import get_session
from .errors import AppError
from .health import RedisDep
from .models import (
    Agent,
    AgentPermission,
    AgentTool,
    AgentVersion,
    AuditEvent,
    DeviceSession,
    InstallationState,
    LoginEvent,
    MembershipRole,
    MfaRecoveryCode,
    PasswordResetToken,
    Permission,
    Role,
    RolePermission,
    User,
    Workspace,
    WorkspaceComputerLimit,
    WorkspaceControlState,
    WorkspaceMembership,
)
from .private_computers import (
    build_computer_templates,
    build_dedicated_computer,
    build_private_computer,
)
from .security import (
    decrypt_secret,
    encrypt_secret,
    generate_recovery_codes,
    generate_token,
    generate_totp_secret,
    hash_password,
    hash_token,
    match_totp_step,
    privacy_hash,
    verify_password,
)

router = APIRouter(prefix="/api/v1", tags=["authentication"])
SessionDep = Annotated[AsyncSession, Depends(get_session)]
DUMMY_PASSWORD_HASH = hash_password("timing-only credential that is never accepted")
RATE_LIMIT_SCRIPT = """
local current = redis.call('INCR', KEYS[1])
if current == 1 then redis.call('EXPIRE', KEYS[1], ARGV[1]) end
return current
"""


class OwnerSetupInput(BaseModel):
    email: str
    password: str = Field(min_length=12, max_length=256)
    display_name: str = Field(min_length=1, max_length=160)
    workspace_name: str = Field(min_length=1, max_length=160)
    timezone: str = Field(min_length=1, max_length=80)

    @field_validator("email")
    @classmethod
    def validate_email(cls, value: str) -> str:
        normalized = value.strip().lower()
        if not re.fullmatch(r"[^@\s]+@[^@\s]+\.[^@\s]+", normalized):
            raise ValueError("A valid email address is required")
        return normalized


class LoginInput(BaseModel):
    email: str = Field(min_length=3, max_length=320)
    password: str = Field(min_length=1, max_length=256)
    remember_device: bool = False
    device_name: str | None = Field(default=None, max_length=160)
    workspace_id: uuid.UUID | None = None
    mfa_code: str | None = Field(default=None, min_length=6, max_length=64)


class PasswordConfirmationInput(BaseModel):
    current_password: str = Field(min_length=1, max_length=256)


class MfaCodeInput(BaseModel):
    code: str = Field(min_length=6, max_length=64)


class MfaDisableInput(PasswordConfirmationInput):
    code: str = Field(min_length=6, max_length=64)


class PasswordChangeInput(PasswordConfirmationInput):
    new_password: str = Field(min_length=12, max_length=256)
    mfa_code: str | None = Field(default=None, min_length=6, max_length=64)


class PasswordResetConfirmInput(BaseModel):
    token: str = Field(min_length=32, max_length=256)
    new_password: str = Field(min_length=12, max_length=256)
    mfa_code: str | None = Field(default=None, min_length=6, max_length=64)


@dataclass(frozen=True, slots=True)
class AuthenticatedSession:
    session_id: uuid.UUID
    workspace_id: uuid.UUID
    user_id: uuid.UUID


def _aware(value: datetime) -> datetime:
    return value if value.tzinfo else value.replace(tzinfo=UTC)


def _mfa_context(user_id: uuid.UUID) -> str:
    return f"hayva:mfa:{user_id}:v1"


def _normalize_recovery_code(value: str) -> str:
    return re.sub(r"[^A-Za-z0-9]", "", value).upper()


def _open_mfa_secret(user: User, *, pending: bool = False) -> str:
    ciphertext = user.mfa_pending_ciphertext if pending else user.mfa_secret_ciphertext
    key_version = user.mfa_pending_key_version if pending else user.mfa_key_version
    if not ciphertext or not key_version:
        raise AppError("MFA_NOT_CONFIGURED", "Multi-factor authentication is unavailable.", 409)
    try:
        return decrypt_secret(
            ciphertext, context=_mfa_context(user.id), key_version=key_version
        )
    except ValueError as error:
        raise AppError(
            "DATA_ENCRYPTION_UNAVAILABLE",
            "Protected authentication data cannot be opened safely.",
            503,
        ) from error


async def _consume_second_factor(
    session: AsyncSession, *, user: User, code: str | None
) -> bool:
    if not user.mfa_enabled_at or not user.mfa_secret_ciphertext or not code:
        return False
    secret = _open_mfa_secret(user)
    matched_step = match_totp_step(
        secret, code, last_used_step=user.mfa_last_used_step
    )
    if matched_step is not None:
        user.mfa_last_used_step = matched_step
        return True
    normalized = _normalize_recovery_code(code)
    if len(normalized) < 16:
        return False
    recovery = await session.scalar(
        select(MfaRecoveryCode).where(
            MfaRecoveryCode.user_id == user.id,
            MfaRecoveryCode.code_hash == hash_token(normalized),
            MfaRecoveryCode.used_at.is_(None),
        ).with_for_update()
    )
    if not recovery:
        return False
    recovery.used_at = datetime.now(UTC)
    return True


def _privacy_hash(value: str | None) -> str | None:
    return privacy_hash(get_settings().require_app_secret(), value)


async def _increment_rate_counter(client, key: str) -> int:
    try:
        return int(await client.eval(RATE_LIMIT_SCRIPT, 1, key, 900))
    except (RedisError, ConnectionError) as error:
        raise AppError(
            "AUTH_RATE_LIMIT_UNAVAILABLE",
            "Authentication is temporarily unavailable.",
            503,
        ) from error


async def _enforce_source_rate_limit(client, *, ip_address: str | None) -> None:
    key = f"auth:ip:{_privacy_hash(ip_address or 'unknown')}"
    if await _increment_rate_counter(client, key) > 30:
        raise AppError("AUTH_RATE_LIMITED", "Too many authentication attempts.", 429)


async def _record_account_failure(client, *, email: str) -> None:
    key = f"auth:account:{_privacy_hash(email)}"
    if await _increment_rate_counter(client, key) > 10:
        raise AppError("AUTH_RATE_LIMITED", "Too many authentication attempts.", 429)


async def _clear_account_failures(client, *, email: str) -> None:
    try:
        await client.delete(f"auth:account:{_privacy_hash(email)}")
    except (RedisError, ConnectionError):
        # A successful credential check is not denied because cleanup became unavailable.
        pass


def _login_event(*, email: str, ip_address: str | None, user_agent: str | None,
                 successful: bool, reason: str, workspace_id: uuid.UUID | None = None,
                 user_id: uuid.UUID | None = None) -> LoginEvent:
    return LoginEvent(
        workspace_id=workspace_id,
        user_id=user_id,
        email_hash=_privacy_hash(email) or "",
        successful=successful,
        reason=reason,
        ip_hash=_privacy_hash(ip_address),
        user_agent=user_agent,
    )


async def _audit(session: AsyncSession, *, workspace_id: uuid.UUID, actor_id: uuid.UUID,
                 event_type: str, request_id: str, data: dict,
                 actor_type: str = "user") -> None:
    workspace = await session.scalar(
        select(Workspace).where(Workspace.id == workspace_id).with_for_update()
    )
    if not workspace:
        raise AppError("WORKSPACE_NOT_FOUND", "The workspace is unavailable.", 404)
    previous = workspace.audit_head_hash
    sequence = workspace.audit_sequence + 1
    event_id = uuid.uuid4()
    event_hash = compute_audit_hash(
        previous_hash=previous,
        sequence=sequence,
        event_id=event_id,
        workspace_id=workspace_id,
        actor_id=actor_id,
        event_type=event_type,
        request_id=request_id,
        data=data,
    )
    workspace.audit_sequence = sequence
    workspace.audit_head_hash = event_hash
    session.add(AuditEvent(id=event_id, workspace_id=workspace_id, sequence=sequence,
                           event_type=event_type, actor_type=actor_type, actor_id=actor_id,
                           request_id=request_id, data=data, previous_hash=previous,
                           event_hash=event_hash))


async def _create_device_session(session: AsyncSession, *, workspace_id: uuid.UUID,
                                  user_id: uuid.UUID, request: Request,
                                  remember_device: bool, device_name: str | None,
                                  session_version: int):
    raw_session, raw_csrf = generate_token(), generate_token()
    lifetime = timedelta(days=30 if remember_device else 1)
    now = datetime.now(UTC)
    absolute_expiry = now + lifetime
    idle_expiry = min(
        absolute_expiry,
        now + timedelta(minutes=get_settings().session_idle_minutes),
    )
    record = DeviceSession(id=uuid.uuid4(), workspace_id=workspace_id, user_id=user_id,
                           token_hash=hash_token(raw_session), csrf_hash=hash_token(raw_csrf),
                           device_name=device_name, user_agent=request.headers.get("user-agent"),
                           ip_hash=_privacy_hash(request.client.host if request.client else None),
                           expires_at=absolute_expiry, idle_expires_at=idle_expiry,
                           last_rotated_at=now, session_version=session_version)
    session.add(record)
    return record, raw_session, raw_csrf, lifetime


def _set_auth_cookies(response: Response, raw_session: str, raw_csrf: str,
                      lifetime: timedelta) -> None:
    secure = get_settings().app_env == "production"
    max_age = int(lifetime.total_seconds())
    response.set_cookie("hayva_session", raw_session, max_age=max_age, httponly=True,
                        secure=secure, samesite="strict", path="/")
    response.set_cookie("hayva_csrf", raw_csrf, max_age=max_age, httponly=False,
                        secure=secure, samesite="strict", path="/")


def _clear_auth_cookies(response: Response) -> None:
    response.delete_cookie("hayva_session", path="/")
    response.delete_cookie("hayva_csrf", path="/")


def _require_csrf(record: DeviceSession | None, cookie_token: str | None,
                  header_token: str | None) -> None:
    if not record or not cookie_token or not header_token:
        raise AppError("CSRF_INVALID", "A valid CSRF token is required.", 403)
    if not hmac.compare_digest(hash_token(cookie_token), record.csrf_hash) or not hmac.compare_digest(
        hash_token(header_token), record.csrf_hash
    ):
        raise AppError("CSRF_INVALID", "A valid CSRF token is required.", 403)


@router.get("/setup/status")
async def setup_status(session: SessionDep):
    installation = await session.get(InstallationState, 1)
    if not installation:
        raise AppError("SETUP_STATE_MISSING", "Installation state is unavailable.", 503)
    return {"setup_required": not installation.owner_setup_completed}


@router.post("/setup/owner", status_code=201)
async def setup_owner(payload: OwnerSetupInput, request: Request, response: Response,
                      session: SessionDep,
                      x_bootstrap_token: Annotated[str | None, Header()] = None):
    expected_token = get_settings().read_owner_bootstrap_token()
    if not expected_token:
        raise AppError("SETUP_NOT_ARMED", "Owner setup is not armed by the operator.", 503)
    if not x_bootstrap_token or not hmac.compare_digest(x_bootstrap_token, expected_token):
        raise AppError("BOOTSTRAP_FORBIDDEN", "A valid bootstrap capability is required.", 403)
    async with session.begin():
        installation = await session.scalar(
            select(InstallationState).where(InstallationState.id == 1).with_for_update()
        )
        if not installation:
            raise AppError("SETUP_STATE_MISSING", "Installation state is unavailable.", 503)
        if installation.owner_setup_completed or await session.scalar(select(func.count(User.id))):
            raise AppError("SETUP_ALREADY_COMPLETED", "Owner setup has already been completed.", 409)
        installation.owner_setup_completed = True
        installation.completed_at = datetime.now(UTC)
        workspace = Workspace(id=uuid.uuid4(), name=payload.workspace_name,
                              timezone=payload.timezone)
        owner = User(id=uuid.uuid4(), email=payload.email,
                     password_hash=hash_password(payload.password),
                     display_name=payload.display_name, auth_version=1,
                     password_changed_at=datetime.now(UTC))
        membership = WorkspaceMembership(id=uuid.uuid4(), workspace_id=workspace.id,
                                         user_id=owner.id, status="active")
        session.add_all([workspace, owner])
        await session.flush()
        session.add(membership)
        session.add(
            WorkspaceControlState(
                workspace_id=workspace.id, emergency_stopped=False, version=1
            )
        )
        session.add(WorkspaceComputerLimit(
            workspace_id=workspace.id,
            maximum_computers=10,
            maximum_running_computers=4,
            maximum_cpu_per_computer=8,
            maximum_memory_mb=16384,
            maximum_disk_gb=200,
        ))
        computer_templates = build_computer_templates(workspace.id)
        session.add_all(computer_templates)
        roles: list[tuple[Role, set[str]]] = []
        existing_permissions = set((await session.scalars(select(Permission.key))).all())
        for key, description in PERMISSIONS.items():
            if key not in existing_permissions:
                session.add(Permission(key=key, description=description))
        for role_name, role_permissions in ROLE_PERMISSIONS.items():
            role = Role(id=uuid.uuid4(), workspace_id=workspace.id, name=role_name, is_system=True)
            session.add(role)
            roles.append((role, role_permissions))
        await session.flush()
        for role, role_permissions in roles:
            for key in role_permissions:
                session.add(RolePermission(workspace_id=workspace.id, role_id=role.id,
                                           permission_key=key))
            if role.name == "Owner":
                session.add(MembershipRole(workspace_id=workspace.id,
                                           membership_id=membership.id, role_id=role.id))
        default_template = next(
            item for item in computer_templates
            if item.template_key == DEFAULT_COMPUTER_TEMPLATE_KEY
        )
        default_computer, default_network_policy = build_dedicated_computer(
            workspace_id=workspace.id,
            created_by_user_id=owner.id,
            agent_name="Personal Executive Assistant",
            template=default_template,
            compute_provider=get_settings().compute_provider,
            cpu_cores=2,
            memory_mb=4096,
            disk_gb=40,
            browser="chromium",
            persistent_disk=True,
            system_privilege="standard_user",
            start_policy="start_when_needed",
            network_policy={
                "internet_access": True,
                "lan_access": False,
                "platform_api_access": True,
                "other_agent_networks": False,
                "allowed_domains": [],
                "blocked_domains": [],
            },
        )
        session.add_all([default_computer, default_network_policy])
        await session.flush()
        default_agent_id = uuid.uuid4()
        default_agent_permissions = [
            "calendar.read", "email.read", "whatsapp.read", "tasks.read", "memory.read",
            "workspace.profile.read", "system.status.read", "computer.observe",
            "computer.control",
        ]
        default_agent_tools = [
            "workspace.profile.read", "system.status.read", "browser.observe",
            "browser.navigate", "browser.wait_for", "browser.extract", "browser.tab_list",
            "browser.screenshot", "browser.click", "browser.type", "browser.press",
            "browser.tab_open", "browser.tab_switch", "browser.tab_close",
        ]
        default_agent = Agent(
            id=default_agent_id,
            workspace_id=workspace.id,
            created_by_user_id=owner.id,
            name="Personal Executive Assistant",
            description="Primary workspace assistant",
            purpose="Act as the user's primary AI assistant within explicit permissions.",
            status="disabled",
            autonomy_level=3,
            computer_required=True,
            computer_id=default_computer.id,
            system_privilege="standard_user",
            computer_autonomy=3,
            computer_network_policy={
                "internet_access": True,
                "lan_access": False,
                "platform_api_access": True,
                "other_agent_networks": False,
                "allowed_domains": [],
                "blocked_domains": [],
            },
            computer_file_policy={"scope": "guest_only", "host_access": False},
            budget_currency="USD",
            version=1,
        )
        session.add(default_agent)
        await session.flush()
        session.add(build_private_computer(
            workspace_id=workspace.id,
            agent_id=default_agent_id,
            computer_id=default_computer.id,
            created_by_user_id=owner.id,
            agent_name=default_agent.name,
            active=False,
        ))
        session.add(AgentVersion(
            id=uuid.uuid4(),
            workspace_id=workspace.id,
            agent_id=default_agent_id,
            version_number=1,
            system_instructions=(
                "Assist the user without exceeding granted tools or permissions."
            ),
            personality="Helpful, concise, careful, and transparent about uncertainty.",
            config_snapshot={
                "name": default_agent.name,
                "status": default_agent.status,
                "autonomy_level": default_agent.autonomy_level,
                "computer_required": True,
                "computer_id": str(default_computer.id),
                "system_privilege": "standard_user",
                "computer_autonomy": 3,
                "tool_names": default_agent_tools,
                "permission_keys": default_agent_permissions,
                "schedule": None,
            },
            created_by_user_id=owner.id,
        ))
        session.add_all([
            AgentPermission(
                workspace_id=workspace.id,
                agent_id=default_agent_id,
                permission_key=key,
            )
            for key in default_agent_permissions
        ])
        session.add_all([
            AgentTool(
                workspace_id=workspace.id,
                agent_id=default_agent_id,
                tool_name=name,
                constraints={},
            )
            for name in default_agent_tools
        ])
        _, raw_session, raw_csrf, lifetime = await _create_device_session(
            session, workspace_id=workspace.id, user_id=owner.id, request=request,
            remember_device=False, device_name="Owner setup", session_version=owner.auth_version,
        )
        await _audit(session, workspace_id=workspace.id, actor_id=owner.id,
                     event_type="owner.setup.completed", request_id=request.state.request_id,
                     data={"workspace_name": workspace.name})
    _set_auth_cookies(response, raw_session, raw_csrf, lifetime)
    return {"success": True, "workspace_id": str(workspace.id), "user_id": str(owner.id)}


@router.post("/auth/login")
async def login(payload: LoginInput, request: Request, response: Response,
                session: SessionDep, redis_client: RedisDep):
    email = payload.email.strip().lower()
    ip_address = request.client.host if request.client else None
    user_agent = request.headers.get("user-agent")
    try:
        await _enforce_source_rate_limit(redis_client, ip_address=ip_address)
    except AppError as error:
        session.add(_login_event(email=email, ip_address=ip_address, user_agent=user_agent,
                                 successful=False, reason=error.code.lower()))
        await session.commit()
        raise
    user = await session.scalar(
        select(User).where(User.email == email, User.is_active.is_(True)).with_for_update()
    )
    candidate_hash = user.password_hash if user else DUMMY_PASSWORD_HASH
    password_valid = verify_password(payload.password, candidate_hash)
    valid = bool(user and password_valid)
    membership = None
    if valid:
        memberships = (await session.scalars(select(WorkspaceMembership).where(
            WorkspaceMembership.user_id == user.id,
            WorkspaceMembership.status == "active"))).all()
        if payload.workspace_id:
            membership = next(
                (item for item in memberships if item.workspace_id == payload.workspace_id), None
            )
        elif len(memberships) == 1:
            membership = memberships[0]
        elif len(memberships) > 1:
            raise AppError(
                "WORKSPACE_SELECTION_REQUIRED",
                "Select an authorized workspace to continue.",
                409,
            )
        valid = membership is not None
    if not valid or not user or not membership:
        try:
            await _record_account_failure(redis_client, email=email)
        except AppError as error:
            session.add(_login_event(email=email, ip_address=ip_address, user_agent=user_agent,
                                     successful=False, reason=error.code.lower()))
            await session.commit()
            raise
        session.add(_login_event(email=email, ip_address=ip_address, user_agent=user_agent,
                                 successful=False, reason="invalid_credentials"))
        await session.commit()
        raise AppError("INVALID_CREDENTIALS", "Email or password is incorrect.", 401)
    if user.mfa_enabled_at:
        if not payload.mfa_code:
            session.add(_login_event(
                email=email, ip_address=ip_address, user_agent=user_agent,
                successful=False, reason="mfa_required",
                workspace_id=membership.workspace_id, user_id=user.id,
            ))
            await session.commit()
            raise AppError(
                "MFA_REQUIRED", "A multi-factor authentication code is required.", 401
            )
        if not await _consume_second_factor(session, user=user, code=payload.mfa_code):
            try:
                await _record_account_failure(redis_client, email=email)
            except AppError as error:
                session.add(_login_event(
                    email=email, ip_address=ip_address, user_agent=user_agent,
                    successful=False, reason=error.code.lower(),
                    workspace_id=membership.workspace_id, user_id=user.id,
                ))
                await session.commit()
                raise
            session.add(_login_event(
                email=email, ip_address=ip_address, user_agent=user_agent,
                successful=False, reason="invalid_mfa",
                workspace_id=membership.workspace_id, user_id=user.id,
            ))
            await session.commit()
            raise AppError("MFA_INVALID", "The authentication code is invalid.", 401)
    await _clear_account_failures(redis_client, email=email)
    record, raw_session, raw_csrf, lifetime = await _create_device_session(
        session, workspace_id=membership.workspace_id, user_id=user.id, request=request,
        remember_device=payload.remember_device, device_name=payload.device_name,
        session_version=user.auth_version)
    session.add(_login_event(email=email, ip_address=ip_address, user_agent=user_agent,
                             successful=True, reason="login",
                             workspace_id=membership.workspace_id, user_id=user.id))
    await _audit(session, workspace_id=membership.workspace_id, actor_id=user.id,
                 event_type="auth.login", request_id=request.state.request_id,
                 data={"session_id": str(record.id)})
    await session.commit()
    _set_auth_cookies(response, raw_session, raw_csrf, lifetime)
    return {"success": True, "workspace_id": str(membership.workspace_id),
            "user": {"id": str(user.id), "display_name": user.display_name}}


async def require_session(
    session: SessionDep,
    hayva_session: Annotated[str | None, Cookie()] = None,
) -> AuthenticatedSession:
    if not hayva_session:
        raise AppError("AUTHENTICATION_REQUIRED", "Authentication is required.", 401)
    now = datetime.now(UTC)
    record = await session.scalar(
        select(DeviceSession)
        .join(User, User.id == DeviceSession.user_id)
        .join(
            WorkspaceMembership,
            and_(
                WorkspaceMembership.workspace_id == DeviceSession.workspace_id,
                WorkspaceMembership.user_id == DeviceSession.user_id,
            ),
        )
        .where(
            DeviceSession.token_hash == hash_token(hayva_session),
            DeviceSession.revoked_at.is_(None),
            DeviceSession.expires_at > now,
            DeviceSession.idle_expires_at > now,
            DeviceSession.session_version == User.auth_version,
            User.is_active.is_(True),
            WorkspaceMembership.status == "active",
        )
    )
    if not record:
        raise AppError("SESSION_INVALID", "The session is invalid or expired.", 401)
    last_seen = _aware(record.last_seen_at)
    if now - last_seen >= timedelta(minutes=5):
        record.last_seen_at = now
        record.idle_expires_at = min(
            _aware(record.expires_at),
            now + timedelta(minutes=get_settings().session_idle_minutes),
        )
        await session.commit()
    return AuthenticatedSession(record.id, record.workspace_id, record.user_id)


AuthDep = Annotated[AuthenticatedSession, Depends(require_session)]


@router.get("/auth/me")
async def current_user(auth: AuthDep, session: SessionDep):
    user = await session.get(User, auth.user_id)
    workspace = await session.get(Workspace, auth.workspace_id)
    if not user or not workspace:
        raise AppError("SESSION_INVALID", "The session is invalid or expired.", 401)
    granted = await permissions_for(session, workspace_id=auth.workspace_id, user_id=auth.user_id)
    return {"success": True, "user": {"id": str(user.id), "email": user.email,
            "display_name": user.display_name, "mfa_enabled": bool(user.mfa_enabled_at)},
            "workspace": {"id": str(workspace.id), "name": workspace.name,
                          "timezone": workspace.timezone},
            "permissions": sorted(granted)}


@router.get("/auth/sessions")
async def list_sessions(auth: AuthDep, session: SessionDep):
    records = (await session.scalars(
        select(DeviceSession).where(DeviceSession.workspace_id == auth.workspace_id,
                                    DeviceSession.user_id == auth.user_id)
        .order_by(DeviceSession.created_at.desc())
    )).all()
    return {"success": True, "sessions": [{"id": str(item.id),
            "device_name": item.device_name, "created_at": item.created_at,
            "last_seen_at": item.last_seen_at, "expires_at": item.expires_at,
            "idle_expires_at": item.idle_expires_at,
            "last_rotated_at": item.last_rotated_at,
            "revoked_at": item.revoked_at, "current": item.id == auth.session_id}
            for item in records]}


@router.post("/auth/logout", status_code=204)
async def logout(request: Request, response: Response, auth: AuthDep, session: SessionDep,
                 hayva_csrf: Annotated[str | None, Cookie()] = None,
                 x_csrf_token: Annotated[str | None, Header()] = None):
    record = await session.get(DeviceSession, auth.session_id)
    _require_csrf(record, hayva_csrf, x_csrf_token)
    record.revoked_at = datetime.now(UTC)
    await _audit(session, workspace_id=auth.workspace_id, actor_id=auth.user_id,
                 event_type="auth.logout", request_id=request.state.request_id,
                 data={"session_id": str(auth.session_id)})
    await session.commit()
    _clear_auth_cookies(response)


@router.post("/auth/logout-all", status_code=204)
async def logout_all(request: Request, response: Response, auth: AuthDep, session: SessionDep,
                     hayva_csrf: Annotated[str | None, Cookie()] = None,
                     x_csrf_token: Annotated[str | None, Header()] = None):
    current = await session.get(DeviceSession, auth.session_id)
    _require_csrf(current, hayva_csrf, x_csrf_token)
    result = await session.execute(
        update(DeviceSession)
        .where(DeviceSession.workspace_id == auth.workspace_id,
               DeviceSession.user_id == auth.user_id,
               DeviceSession.revoked_at.is_(None))
        .values(revoked_at=datetime.now(UTC))
    )
    await _audit(session, workspace_id=auth.workspace_id, actor_id=auth.user_id,
                 event_type="auth.logout_all", request_id=request.state.request_id,
                 data={"revoked_sessions": result.rowcount})
    await session.commit()
    _clear_auth_cookies(response)


@router.post("/auth/session/rotate")
async def rotate_session(
    request: Request, response: Response, auth: AuthDep, session: SessionDep,
    hayva_csrf: Annotated[str | None, Cookie()] = None,
    x_csrf_token: Annotated[str | None, Header()] = None,
):
    record = await session.scalar(
        select(DeviceSession).where(
            DeviceSession.id == auth.session_id,
            DeviceSession.workspace_id == auth.workspace_id,
            DeviceSession.user_id == auth.user_id,
            DeviceSession.revoked_at.is_(None),
        ).with_for_update()
    )
    _require_csrf(record, hayva_csrf, x_csrf_token)
    now = datetime.now(UTC)
    raw_session, raw_csrf = generate_token(), generate_token()
    record.token_hash = hash_token(raw_session)
    record.csrf_hash = hash_token(raw_csrf)
    record.last_rotated_at = now
    record.last_seen_at = now
    record.idle_expires_at = min(
        _aware(record.expires_at),
        now + timedelta(minutes=get_settings().session_idle_minutes),
    )
    await _audit(
        session, workspace_id=auth.workspace_id, actor_id=auth.user_id,
        event_type="auth.session_rotated", request_id=request.state.request_id,
        data={"session_id": str(record.id)},
    )
    await session.commit()
    _set_auth_cookies(response, raw_session, raw_csrf, _aware(record.expires_at) - now)
    return {"success": True, "session_id": str(record.id)}


@router.post("/auth/sessions/{session_id}/revoke", status_code=204)
async def revoke_session(
    session_id: uuid.UUID, request: Request, response: Response, auth: AuthDep,
    session: SessionDep,
    hayva_csrf: Annotated[str | None, Cookie()] = None,
    x_csrf_token: Annotated[str | None, Header()] = None,
):
    current = await session.get(DeviceSession, auth.session_id)
    _require_csrf(current, hayva_csrf, x_csrf_token)
    target = await session.scalar(select(DeviceSession).where(
        DeviceSession.id == session_id,
        DeviceSession.workspace_id == auth.workspace_id,
        DeviceSession.user_id == auth.user_id,
    ).with_for_update())
    if not target:
        raise AppError("SESSION_NOT_FOUND", "The device session is unavailable.", 404)
    if not target.revoked_at:
        target.revoked_at = datetime.now(UTC)
        await _audit(
            session, workspace_id=auth.workspace_id, actor_id=auth.user_id,
            event_type="auth.session_revoked", request_id=request.state.request_id,
            data={"session_id": str(target.id), "current": target.id == auth.session_id},
        )
        await session.commit()
    if target.id == auth.session_id:
        _clear_auth_cookies(response)


@router.post("/auth/mfa/setup")
async def setup_mfa(
    payload: PasswordConfirmationInput, request: Request, auth: AuthDep, session: SessionDep,
    hayva_csrf: Annotated[str | None, Cookie()] = None,
    x_csrf_token: Annotated[str | None, Header()] = None,
):
    current = await session.get(DeviceSession, auth.session_id)
    _require_csrf(current, hayva_csrf, x_csrf_token)
    user = await session.scalar(
        select(User).where(User.id == auth.user_id).with_for_update()
    )
    if not user or not verify_password(payload.current_password, user.password_hash):
        raise AppError(
            "AUTH_REAUTHENTICATION_FAILED", "Current credentials could not be verified.", 403
        )
    if user.mfa_enabled_at:
        raise AppError("MFA_ALREADY_ENABLED", "Multi-factor authentication is already enabled.", 409)
    secret = generate_totp_secret()
    try:
        ciphertext, key_version = encrypt_secret(secret, context=_mfa_context(user.id))
    except ValueError as error:
        raise AppError(
            "DATA_ENCRYPTION_UNAVAILABLE",
            "Protected authentication storage is not configured.",
            503,
        ) from error
    user.mfa_pending_ciphertext = ciphertext
    user.mfa_pending_key_version = key_version
    user.mfa_pending_expires_at = datetime.now(UTC) + timedelta(minutes=10)
    await _audit(
        session, workspace_id=auth.workspace_id, actor_id=auth.user_id,
        event_type="auth.mfa_setup_started", request_id=request.state.request_id, data={},
    )
    await session.commit()
    settings = get_settings()
    label = quote(f"{settings.mfa_issuer}:{user.email}")
    query = urlencode({
        "secret": secret, "issuer": settings.mfa_issuer,
        "algorithm": "SHA1", "digits": 6, "period": 30,
    })
    return {
        "success": True,
        "secret": secret,
        "provisioning_uri": f"otpauth://totp/{label}?{query}",
        "expires_at": user.mfa_pending_expires_at,
    }


@router.post("/auth/mfa/verify")
async def verify_mfa_setup(
    payload: MfaCodeInput, request: Request, auth: AuthDep, session: SessionDep,
    hayva_csrf: Annotated[str | None, Cookie()] = None,
    x_csrf_token: Annotated[str | None, Header()] = None,
):
    current = await session.get(DeviceSession, auth.session_id)
    _require_csrf(current, hayva_csrf, x_csrf_token)
    user = await session.scalar(
        select(User).where(User.id == auth.user_id).with_for_update()
    )
    now = datetime.now(UTC)
    if (
        not user
        or not user.mfa_pending_expires_at
        or _aware(user.mfa_pending_expires_at) <= now
    ):
        raise AppError("MFA_SETUP_EXPIRED", "Start multi-factor setup again.", 409)
    secret = _open_mfa_secret(user, pending=True)
    matched_step = match_totp_step(secret, payload.code)
    if matched_step is None:
        raise AppError("MFA_INVALID", "The authentication code is invalid.", 401)
    user.mfa_secret_ciphertext = user.mfa_pending_ciphertext
    user.mfa_key_version = user.mfa_pending_key_version
    user.mfa_enabled_at = now
    user.mfa_last_used_step = matched_step
    user.mfa_pending_ciphertext = None
    user.mfa_pending_key_version = None
    user.mfa_pending_expires_at = None
    await session.execute(delete(MfaRecoveryCode).where(MfaRecoveryCode.user_id == user.id))
    recovery_codes = generate_recovery_codes()
    session.add_all([
        MfaRecoveryCode(
            id=uuid.uuid4(), user_id=user.id,
            code_hash=hash_token(_normalize_recovery_code(code)),
        )
        for code in recovery_codes
    ])
    revoked = await session.execute(update(DeviceSession).where(
        DeviceSession.user_id == user.id,
        DeviceSession.id != auth.session_id,
        DeviceSession.revoked_at.is_(None),
    ).values(revoked_at=now))
    await _audit(
        session, workspace_id=auth.workspace_id, actor_id=auth.user_id,
        event_type="auth.mfa_enabled", request_id=request.state.request_id,
        data={"other_sessions_revoked": revoked.rowcount},
    )
    await session.commit()
    return {"success": True, "recovery_codes": recovery_codes}


@router.post("/auth/mfa/recovery-codes")
async def regenerate_mfa_recovery_codes(
    payload: MfaDisableInput, request: Request, auth: AuthDep, session: SessionDep,
    hayva_csrf: Annotated[str | None, Cookie()] = None,
    x_csrf_token: Annotated[str | None, Header()] = None,
):
    current = await session.get(DeviceSession, auth.session_id)
    _require_csrf(current, hayva_csrf, x_csrf_token)
    user = await session.scalar(
        select(User).where(User.id == auth.user_id).with_for_update()
    )
    if not user or not verify_password(payload.current_password, user.password_hash):
        raise AppError(
            "AUTH_REAUTHENTICATION_FAILED", "Current credentials could not be verified.", 403
        )
    if not await _consume_second_factor(session, user=user, code=payload.code):
        raise AppError("MFA_INVALID", "The authentication code is invalid.", 401)
    await session.execute(delete(MfaRecoveryCode).where(MfaRecoveryCode.user_id == user.id))
    recovery_codes = generate_recovery_codes()
    session.add_all([
        MfaRecoveryCode(
            id=uuid.uuid4(), user_id=user.id,
            code_hash=hash_token(_normalize_recovery_code(code)),
        )
        for code in recovery_codes
    ])
    await _audit(
        session, workspace_id=auth.workspace_id, actor_id=auth.user_id,
        event_type="auth.mfa_recovery_codes_rotated", request_id=request.state.request_id, data={},
    )
    await session.commit()
    return {"success": True, "recovery_codes": recovery_codes}


@router.post("/auth/mfa/disable")
async def disable_mfa(
    payload: MfaDisableInput, request: Request, response: Response, auth: AuthDep,
    session: SessionDep,
    hayva_csrf: Annotated[str | None, Cookie()] = None,
    x_csrf_token: Annotated[str | None, Header()] = None,
):
    current = await session.get(DeviceSession, auth.session_id)
    _require_csrf(current, hayva_csrf, x_csrf_token)
    user = await session.scalar(
        select(User).where(User.id == auth.user_id).with_for_update()
    )
    if not user or not verify_password(payload.current_password, user.password_hash):
        raise AppError(
            "AUTH_REAUTHENTICATION_FAILED", "Current credentials could not be verified.", 403
        )
    if not await _consume_second_factor(session, user=user, code=payload.code):
        raise AppError("MFA_INVALID", "The authentication code is invalid.", 401)
    now = datetime.now(UTC)
    user.mfa_secret_ciphertext = None
    user.mfa_key_version = None
    user.mfa_enabled_at = None
    user.mfa_last_used_step = None
    user.auth_version += 1
    await session.execute(delete(MfaRecoveryCode).where(MfaRecoveryCode.user_id == user.id))
    await session.execute(update(DeviceSession).where(
        DeviceSession.user_id == user.id,
        DeviceSession.revoked_at.is_(None),
    ).values(revoked_at=now))
    _, raw_session, raw_csrf, lifetime = await _create_device_session(
        session, workspace_id=auth.workspace_id, user_id=user.id, request=request,
        remember_device=False, device_name=current.device_name if current else "Security change",
        session_version=user.auth_version,
    )
    await _audit(
        session, workspace_id=auth.workspace_id, actor_id=auth.user_id,
        event_type="auth.mfa_disabled", request_id=request.state.request_id, data={},
    )
    await session.commit()
    _set_auth_cookies(response, raw_session, raw_csrf, lifetime)
    return {"success": True}


@router.post("/auth/password/change")
async def change_password(
    payload: PasswordChangeInput, request: Request, response: Response, auth: AuthDep,
    session: SessionDep,
    hayva_csrf: Annotated[str | None, Cookie()] = None,
    x_csrf_token: Annotated[str | None, Header()] = None,
):
    current = await session.get(DeviceSession, auth.session_id)
    _require_csrf(current, hayva_csrf, x_csrf_token)
    user = await session.scalar(
        select(User).where(User.id == auth.user_id).with_for_update()
    )
    if not user or not verify_password(payload.current_password, user.password_hash):
        raise AppError(
            "AUTH_REAUTHENTICATION_FAILED", "Current credentials could not be verified.", 403
        )
    if verify_password(payload.new_password, user.password_hash):
        raise AppError("PASSWORD_REUSE", "Choose a different password.", 409)
    if user.mfa_enabled_at and not await _consume_second_factor(
        session, user=user, code=payload.mfa_code
    ):
        raise AppError("MFA_REQUIRED", "A valid multi-factor code is required.", 401)
    now = datetime.now(UTC)
    user.password_hash = hash_password(payload.new_password)
    user.password_changed_at = now
    user.auth_version += 1
    await session.execute(update(DeviceSession).where(
        DeviceSession.user_id == user.id,
        DeviceSession.revoked_at.is_(None),
    ).values(revoked_at=now))
    await session.execute(update(PasswordResetToken).where(
        PasswordResetToken.user_id == user.id,
        PasswordResetToken.used_at.is_(None),
    ).values(used_at=now))
    _, raw_session, raw_csrf, lifetime = await _create_device_session(
        session, workspace_id=auth.workspace_id, user_id=user.id, request=request,
        remember_device=False, device_name=current.device_name if current else "Password changed",
        session_version=user.auth_version,
    )
    await _audit(
        session, workspace_id=auth.workspace_id, actor_id=auth.user_id,
        event_type="auth.password_changed", request_id=request.state.request_id, data={},
    )
    await session.commit()
    _set_auth_cookies(response, raw_session, raw_csrf, lifetime)
    return {"success": True}


@router.post("/auth/password-reset/confirm")
async def confirm_password_reset(
    payload: PasswordResetConfirmInput, request: Request, session: SessionDep,
    redis_client: RedisDep,
):
    ip_address = request.client.host if request.client else None
    await _enforce_source_rate_limit(redis_client, ip_address=ip_address)
    now = datetime.now(UTC)
    reset = await session.scalar(select(PasswordResetToken).where(
        PasswordResetToken.token_hash == hash_token(payload.token),
        PasswordResetToken.used_at.is_(None),
    ).with_for_update())
    if not reset or _aware(reset.expires_at) <= now:
        raise AppError(
            "PASSWORD_RESET_INVALID", "The password reset capability is invalid or expired.", 401
        )
    user = await session.scalar(
        select(User).where(User.id == reset.user_id, User.is_active.is_(True)).with_for_update()
    )
    if not user:
        raise AppError(
            "PASSWORD_RESET_INVALID", "The password reset capability is invalid or expired.", 401
        )
    if user.mfa_enabled_at and not await _consume_second_factor(
        session, user=user, code=payload.mfa_code
    ):
        raise AppError("MFA_REQUIRED", "A valid multi-factor code is required.", 401)
    user.password_hash = hash_password(payload.new_password)
    user.password_changed_at = now
    user.auth_version += 1
    await session.execute(update(DeviceSession).where(
        DeviceSession.user_id == user.id,
        DeviceSession.revoked_at.is_(None),
    ).values(revoked_at=now))
    await session.execute(update(PasswordResetToken).where(
        PasswordResetToken.user_id == user.id,
        PasswordResetToken.used_at.is_(None),
    ).values(used_at=now))
    await _audit(
        session, workspace_id=reset.workspace_id, actor_id=user.id,
        event_type="auth.password_reset_completed", request_id=request.state.request_id,
        data={"reset_token_id": str(reset.id)},
    )
    await session.commit()
    return {"success": True}
