# Research: FSM паттерны в aiogram 3.x

## Хранилище состояний (Storage)

### Выбор по фазе разработки

| Фаза | Storage | Плюсы | Минусы |
|------|---------|-------|--------|
| Разработка | `MemoryStorage` | Нет зависимостей | Теряется при рестарте |
| MVP | `SQLiteStorage` | Персистентен, нет Redis | Не распределённый |
| Продакшн | `RedisStorage` | Распределённый, быстрый | Нужен Redis |

```python
# Разработка
from aiogram.fsm.storage.memory import MemoryStorage
storage = MemoryStorage()

# MVP — pip install aiogram-sqlite-storage
from aiogram_sqlite_storage.sqlitestore import SQLStorage
storage = SQLStorage(db_path="fsm.db")

# Продакшн
from aiogram.fsm.storage.redis import RedisStorage
from redis.asyncio import Redis
storage = RedisStorage(redis=Redis(host="localhost"))

dp = Dispatcher(storage=storage)
```

---

## Структура StateData

### Правило: хранить только параметры текущего шага

```python
# ПРАВИЛЬНО: только то что нужно в следующем шаге
await state.update_data(
    group_id=group_id,      # ID группы для следующего шага
    pending_name="Чат 2",   # временное имя до подтверждения
)

# НЕПРАВИЛЬНО: дублировать данные из БД
await state.update_data(
    user_name="Иван",       # уже есть в БД
    all_chats=[...],        # большой объект, лучше запрашивать из БД
)
```

### Типичная StateData для онбординга

```python
# Шаг 1: узнали group_id
await state.update_data(group_id=123456789, started_at=datetime.now().isoformat())

# Шаг 2: узнали имя пользователя
await state.update_data(display_name="Иван")

# Финал: читаем всё
data = await state.get_data()
group_id = data["group_id"]
display_name = data.get("display_name", "Пользователь")
await state.clear()  # очистить после завершения
```

---

## Кастомные фильтры

```python
from aiogram.filters import BaseFilter
from aiogram.types import Message

class IsForumTopicFilter(BaseFilter):
    """True если сообщение отправлено в тему Forum-группы."""
    async def __call__(self, message: Message) -> bool:
        return message.message_thread_id is not None


class IsGroupAdminFilter(BaseFilter):
    """True если пользователь является администратором группы."""
    async def __call__(self, message: Message, bot) -> bool:
        if message.chat.type not in ("group", "supergroup"):
            return False
        member = await bot.get_chat_member(message.chat.id, message.from_user.id)
        return member.status in ("administrator", "creator")


# Использование:
@router.message(IsForumTopicFilter())
async def on_topic_message(message: Message): ...
```

---

## Обработка ошибок в FSM

```python
from aiogram import Router
from aiogram.types import Message, ErrorEvent

error_router = Router()

@error_router.error()
async def handle_error(event: ErrorEvent) -> None:
    """Глобальный обработчик ошибок."""
    update = event.update
    exception = event.exception

    # Логируем
    import logging
    logging.error(f"Error handling update: {exception}", exc_info=True)

    # Пытаемся уведомить пользователя
    if update.message:
        await update.message.answer(
            "Что-то пошло не так. Попробуй ещё раз или напиши /start"
        )
```

---

## Выводы для нашей реализации

1. **MVP**: `SQLiteStorage` — персистентен, нет зависимостей от Redis
2. **StateData**: хранить только `group_id`, `thread_id` текущей операции
3. **Timeout**: сохранять `started_at`, проверять при каждом шаге
4. **Очищать state**: `await state.clear()` после завершения онбординга
5. **Ошибки**: глобальный `@error_router.error()` для graceful degradation
