openapi: 3.0.3
info:
  title: Trektik API
  description: |
    Trektik — текущая реализованная система продуктовой аналитики, A/B-тестов и feature flags. Этот файл описывает фактический API, а не wishful spec.

    ## Аутентификация

    Trektik поддерживает два способа аутентификации:

    ### 1. JWT (для UI и внутренних клиентов)
    - `POST /auth/login` → получить `access_token`
    - Передавать в заголовке: `Authorization: Bearer <access_token>`
    - Обновлять по `POST /auth/refresh` с `refresh_token`

    ### 2. Secret Key (для Public API и Management API)
    - Создаётся в Settings → API Tokens
    - Передавать в заголовке: `X-Secret-Key: sk_...`
    - Или: `Authorization: Bearer sk_...`

    ### 3. API Key (для Ingestion API)
    - Передавать в заголовке: `X-API-Key: pk_...`
    - `Authorization: Bearer <key>` принимается текущей реализацией как fallback

    **Scopes (Secret Key):**
    - `query` — только Query API (аналитика)
    - `read` — чтение (GET endpoints)
    - `write` — чтение + запись (GET + POST + PUT)
    - `admin` — полный доступ (включая DELETE)

    ## Rate Limiting
    - Заголовки: `X-RateLimit-Limit`, `X-RateLimit-Remaining`
    - При превышении: 429 + `Retry-After: 60`

    ## Ошибки
    Все ошибки возвращаются в формате:
    ```json
    {"error": {"code": "error_code", "message": "Human-readable message"}}
    ```

    | HTTP Code | Описание |
    |-----------|----------|
    | 400 | Неверный запрос |
    | 401 | Не авторизован |
    | 403 | Недостаточно прав |
    | 404 | Не найдено |
    | 429 | Превышен лимит запросов |
    | 500 | Внутренняя ошибка |
  version: 2.0.0
  contact:
    name: Trektik Support
    url: https://trektik.ru

servers:
  - url: https://api.trektik.ru
    description: Production
  - url: https://collect.trektik.ru
    description: Production collector
  - url: http://localhost:8080
    description: Local API
  - url: http://localhost:8081
    description: Local collector

tags:
  - name: Auth
    description: Регистрация, логин, 2FA, сброс пароля
  - name: Ingestion
    description: Приём событий (track, batch, identify, alias)
  - name: Replay
    description: Session replay ingestion и просмотр сессий
  - name: Heatmaps
    description: Сбор и просмотр click / scroll / attention heatmaps
  - name: Query
    description: Аналитические запросы (events, funnels, retention, journeys, revenue)
  - name: Segments
    description: Сегменты пользователей
  - name: Dashboards
    description: Дашборды и виджеты
  - name: Alerts
    description: Алерты и уведомления
  - name: Experiments
    description: A/B-тесты и multi-armed bandits
  - name: Feature Flags
    description: Флаги функций и progressive rollout
  - name: Rollout Plans
    description: Планы постепенного раскатывания
  - name: Mutex Groups
    description: Взаимоисключающие группы экспериментов
  - name: Export
    description: Экспорт данных (events, users, cohorts)
  - name: Projects
    description: Управление проектами
  - name: API Keys
    description: Управление API-ключами
  - name: Webhooks
    description: Управление вебхуками
  - name: Integrations
    description: Telegram, Slack, Kafka интеграции
  - name: Taxonomy
    description: Tracking Plan — каталог событий
  - name: Data Quality
    description: Качество данных
  - name: Users
    description: Профили пользователей
  - name: Usage
    description: Лимиты и потребление
  - name: Management API
    description: Программный доступ через Secret Key

security: []

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: "JWT access token из /auth/login"
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: "Project API key (pk_...). Используется collector'ом и SDK."
    SecretKey:
      type: apiKey
      in: header
      name: X-Secret-Key
      description: "Secret Key (sk_...). Создаётся в Settings → API Tokens."

  schemas:
    Error:
      type: object
      properties:
        error:
          type: object
          properties:
            code: { type: string, example: "bad_request" }
            message: { type: string, example: "project_id is required" }

    EventQuery:
      type: object
      required: [event_name]
      properties:
        event_name: { type: string, example: "page_view" }
        filters:
          type: array
          items:
            type: object
            properties:
              property: { type: string, example: "browser" }
              operator: { type: string, enum: [is, is_not, contains, gt, lt] }
              value: { type: string, example: "Chrome" }

    QueryRequest:
      type: object
      required: [project_id, events, date_range]
      properties:
        project_id: { type: integer, example: 1 }
        events:
          type: array
          items: { $ref: '#/components/schemas/EventQuery' }
        date_range:
          type: object
          required: [start, end]
          properties:
            start: { type: string, format: date, example: "2026-03-01" }
            end: { type: string, format: date, example: "2026-03-15" }
        metric: { type: string, enum: [total, uniq, avg, sum, min, max, median, p90, p99], default: total }
        interval: { type: string, enum: [hour, day, week, month], default: day }
        group_by:
          type: array
          items:
            type: object
            properties:
              property: { type: string }
              type: { type: string, enum: [str, num] }
        limit: { type: integer, default: 10000, maximum: 50000 }
        offset: { type: integer, default: 0 }

    FunnelRequest:
      type: object
      required: [project_id, steps, date_range]
      properties:
        project_id: { type: integer }
        steps:
          type: array
          items:
            type: object
            required: [event_name]
            properties:
              event_name: { type: string }
        date_range:
          type: object
          properties:
            start: { type: string, format: date }
            end: { type: string, format: date }
        conversion_window: { type: string, default: "7d" }

    RetentionRequest:
      type: object
      required: [project_id, start_event, return_event, date_range]
      properties:
        project_id: { type: integer }
        start_event: { type: string }
        return_event: { type: string }
        date_range:
          type: object
          properties:
            start: { type: string, format: date }
            end: { type: string, format: date }
        granularity: { type: string, enum: [day, week, month], default: week }

    JourneyRequest:
      type: object
      required: [project_id, date_range]
      properties:
        project_id: { type: integer }
        start_event: { type: string }
        date_range:
          type: object
          properties:
            start: { type: string, format: date }
            end: { type: string, format: date }
        max_steps: { type: integer, default: 5 }

    TrackRequest:
      type: object
      required: [event_name]
      properties:
        event_name: { type: string, example: "page_view" }
        user_id: { type: string, example: "user_123" }
        device_id: { type: string, example: "dev_abc" }
        session_id: { type: string }
        group_id: { type: string }
        timestamp: { type: string, format: date-time }
        insert_id: { type: string, description: "Idempotency key" }
        str_properties:
          type: object
          additionalProperties: { type: string }
          example: { browser: "Chrome", os: "macOS" }
        num_properties:
          type: object
          additionalProperties: { type: number }
          example: { price: 29.99 }
        experiments:
          type: object
          additionalProperties: { type: string }
        screen_width: { type: integer }
        screen_height: { type: integer }
        utm_source: { type: string }
        utm_medium: { type: string }
        utm_campaign: { type: string }
        page_url: { type: string }
        page_path: { type: string }
        referrer: { type: string }
        sdk_version: { type: string }
        platform: { type: string }

    AlertChannel:
      type: object
      required: [type, target]
      properties:
        type: { type: string, enum: [email, telegram, telegram_group, slack, webhook] }
        target: { type: string, description: "Email, chat_id, channel, or webhook URL" }

    Alert:
      type: object
      properties:
        id: { type: integer }
        project_id: { type: integer }
        created_by: { type: integer }
        name: { type: string }
        type: { type: string, enum: [threshold, anomaly, segment_size] }
        metric: { type: string }
        event_name: { type: string, nullable: true }
        segment_id: { type: integer, nullable: true }
        condition: { type: string }
        threshold: { type: number }
        window_minutes: { type: integer }
        channels:
          type: array
          items: { $ref: '#/components/schemas/AlertChannel' }
        is_active: { type: boolean }
        cooldown_min: { type: integer }
        last_triggered: { type: string, format: date-time, nullable: true }
        created_at: { type: string, format: date-time }

    Segment:
      type: object
      properties:
        id: { type: integer }
        project_id: { type: integer }
        name: { type: string }
        description: { type: string }
        type: { type: string, enum: [behavioral, property, combined] }
        rules: { type: object }
        is_dynamic: { type: boolean }
        user_count: { type: integer }
        created_at: { type: string, format: date-time }

    Dashboard:
      type: object
      properties:
        id: { type: integer }
        project_id: { type: integer }
        name: { type: string }
        description: { type: string }
        layout: { type: object, nullable: true }
        is_default: { type: boolean }
        sharing: { type: string }
        created_at: { type: string, format: date-time }

    DashboardWidget:
      type: object
      properties:
        id: { type: integer }
        dashboard_id: { type: integer }
        report_id: { type: integer, nullable: true }
        widget_type: { type: string, enum: [chart, number, table, funnel, retention, text] }
        position: { type: object, description: "{x, y, w, h}" }
        config: { type: object, nullable: true }

    Experiment:
      type: object
      properties:
        id: { type: integer }
        project_id: { type: integer }
        key: { type: string }
        name: { type: string }
        description: { type: string }
        hypothesis: { type: string }
        status: { type: string, enum: [draft, running, paused, completed] }
        traffic_pct: { type: integer }
        primary_metric: { type: string }
        experiment_type: { type: string, enum: [ab_test, bandit] }
        confidence_level: { type: number }
        statistics_method: { type: string, enum: [fixed, sequential] }
        variants:
          type: array
          items:
            type: object
            properties:
              id: { type: integer }
              key: { type: string }
              name: { type: string }
              weight: { type: integer }
              is_control: { type: boolean }
        started_at: { type: string, format: date-time, nullable: true }
        created_at: { type: string, format: date-time }

    FeatureFlag:
      type: object
      properties:
        id: { type: integer }
        project_id: { type: integer }
        key: { type: string }
        name: { type: string }
        description: { type: string }
        type: { type: string, enum: [boolean, multivariate, percentage] }
        is_active: { type: boolean }
        rollout_pct: { type: integer }
        default_value: {}
        targeting_rules: { type: object, nullable: true }
        variants: { type: array, items: { type: object }, nullable: true }
        created_at: { type: string, format: date-time }

    RolloutPlan:
      type: object
      properties:
        id: { type: integer }
        flag_id: { type: integer }
        project_id: { type: integer }
        stages:
          type: array
          items:
            type: object
            properties:
              percentage: { type: integer }
              delay_hours: { type: integer }
        current_stage: { type: integer }
        status: { type: string, enum: [active, paused, completed, rolled_back] }
        pause_on_alert_id: { type: integer, nullable: true }
        started_at: { type: string, format: date-time }

paths:
  # ═══════════════════════════════════════════════════════════════
  # Auth
  # ═══════════════════════════════════════════════════════════════

  /auth/register:
    post:
      summary: Регистрация
      tags: [Auth]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, password, name]
              properties:
                email: { type: string, format: email, example: "user@example.com" }
                password: { type: string, minLength: 8, example: "secureP@ss123" }
                name: { type: string, example: "Alex" }
            examples:
              basic:
                summary: "curl — регистрация"
                value: { email: "user@example.com", password: "secureP@ss123", name: "Alex" }
      responses:
        '201':
          description: Аккаунт создан
          content:
            application/json:
              example:
                access_token: "eyJhbGciOiJI..."
                refresh_token: "eyJhbGciOiJI..."
                account: { id: 1, email: "user@example.com", name: "Alex" }
        '400': { description: "Email уже занят или невалидные данные", content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }

  /auth/login:
    post:
      summary: Логин
      description: |
        Возвращает JWT tokens. Если включён 2FA, вернёт `mfa_required: true` и `mfa_token` — используйте `/auth/2fa/login`.
      tags: [Auth]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, password]
              properties:
                email: { type: string, format: email }
                password: { type: string }
            examples:
              basic:
                summary: "curl — логин"
                value: { email: "user@example.com", password: "secureP@ss123" }
      responses:
        '200':
          description: Успешный логин
          content:
            application/json:
              examples:
                success:
                  summary: Обычный логин
                  value: { access_token: "eyJ...", refresh_token: "eyJ...", expires_in: 3600, account: { id: 1, email: "user@example.com", name: "Alex" } }
                mfa:
                  summary: Требуется 2FA
                  value: { mfa_required: true, mfa_token: "mfa_abc123..." }
        '401': { description: Неверный email или пароль }

  /auth/2fa/login:
    post:
      summary: 2FA логин
      description: Завершает логин при включённом 2FA. Принимает mfa_token из `/auth/login` и TOTP-код.
      tags: [Auth]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [mfa_token, code]
              properties:
                mfa_token: { type: string }
                code: { type: string, example: "123456", description: "6-значный TOTP код или 8-символьный recovery code" }
      responses:
        '200':
          description: Успех
          content:
            application/json:
              example: { access_token: "eyJ...", refresh_token: "eyJ...", expires_in: 3600, account: { id: 1, email: "user@example.com" } }
        '401': { description: Неверный код }

  /auth/refresh:
    post:
      summary: Обновить токен
      tags: [Auth]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [refresh_token]
              properties:
                refresh_token: { type: string }
      responses:
        '200':
          description: Новая пара токенов
          content:
            application/json:
              example: { access_token: "eyJ...", refresh_token: "eyJ...", expires_in: 3600 }
        '401': { description: Невалидный refresh token }

  /auth/logout:
    post:
      summary: Выход
      tags: [Auth]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [refresh_token]
              properties:
                refresh_token: { type: string }
      responses:
        '200':
          content:
            application/json:
              example: { status: "ok" }

  /auth/forgot-password:
    post:
      summary: Запрос сброса пароля
      description: Всегда возвращает 200 (для безопасности).
      tags: [Auth]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email: { type: string, format: email }
      responses:
        '200':
          content:
            application/json:
              example: { status: "ok" }

  /auth/reset-password:
    post:
      summary: Сброс пароля
      tags: [Auth]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [token, new_password]
              properties:
                token: { type: string }
                new_password: { type: string, minLength: 8 }
      responses:
        '200':
          content:
            application/json:
              example: { status: "ok" }
        '400': { description: Невалидный или просроченный токен }

  /auth/verify-email:
    post:
      summary: Подтверждение email
      tags: [Auth]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [token]
              properties:
                token: { type: string }
      responses:
        '200':
          content:
            application/json:
              example: { status: "email_verified" }

  /auth/2fa/enable:
    post:
      summary: Включить 2FA
      tags: [Auth]
      security: [{ BearerAuth: [] }]
      responses:
        '200':
          description: TOTP secret и QR-код
          content:
            application/json:
              example: { secret: "JBSWY3DPEHPK3PXP", qr_code: "otpauth://totp/Trektik:user@example.com?secret=..." }

  /auth/2fa/verify:
    post:
      summary: Подтвердить 2FA
      tags: [Auth]
      security: [{ BearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [secret, code]
              properties:
                secret: { type: string }
                code: { type: string, example: "123456" }
      responses:
        '200':
          content:
            application/json:
              example: { status: "2fa_enabled" }

  /auth/2fa/disable:
    post:
      summary: Отключить 2FA
      tags: [Auth]
      security: [{ BearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [password]
              properties:
                password: { type: string }
      responses:
        '200':
          content:
            application/json:
              example: { status: "2fa_disabled" }

  # ═══════════════════════════════════════════════════════════════
  # Ingestion
  # ═══════════════════════════════════════════════════════════════

  /v1/track:
    post:
      summary: Отправить событие
      description: |
        Основной endpoint collector'а. Требуется `event_name` и хотя бы один из `user_id` / `device_id`. Успешный ответ всегда `200 OK`.

        ```bash
        curl -X POST http://localhost:8081/v1/track \
          -H "Content-Type: application/json" \
          -H "X-API-Key: pk_..." \
          -d '{"event_name":"page_view","user_id":"user_123","str_properties":{"page":"/home"}}'
        ```
      tags: [Ingestion]
      security: [{ ApiKeyAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/TrackRequest' }
            examples:
              pageview:
                summary: Page view
                value:
                  event_name: "page_view"
                  user_id: "user_123"
                  str_properties: { page: "/pricing", browser: "Chrome" }
              purchase:
                summary: Purchase
                value:
                  event_name: "purchase"
                  user_id: "user_123"
                  num_properties: { amount: 49.99, items_count: 2 }
                  str_properties: { currency: "USD" }
      responses:
        '200':
          content:
            application/json:
              examples:
                ok: { value: { status: "ok" } }
                duplicate: { value: { status: "ok", duplicate: true } }
        '400': { description: "Отсутствует event_name или user_id/device_id" }
        '429': { description: "Rate limit" }

  /v1/batch:
    post:
      summary: Пакетная отправка событий
      description: |
        До 1000 событий за один запрос.

        ```bash
        curl -X POST http://localhost:8081/v1/batch \
          -H "Content-Type: application/json" \
          -H "X-API-Key: pk_..." \
          -d '{"events":[{"event_name":"page_view","user_id":"u1"},{"event_name":"click","user_id":"u2"}]}'
        ```
      tags: [Ingestion]
      security: [{ ApiKeyAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [events]
              properties:
                events:
                  type: array
                  items: { $ref: '#/components/schemas/TrackRequest' }
                  maxItems: 1000
      responses:
        '200':
          content:
            application/json:
              example: { status: "ok", accepted: 2 }
        '400': { description: "Невалидные события" }

  /v1/identify:
    post:
      summary: Идентификация пользователя
      description: Связывает `device_id` с `user_id`.
      tags: [Ingestion]
      security: [{ ApiKeyAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [user_id, device_id]
              properties:
                user_id: { type: string }
                device_id: { type: string }
                user_properties:
                  type: object
                  additionalProperties: { type: string }
            examples:
              basic:
                value: { user_id: "user_123", device_id: "device_abc", user_properties: { name: "Alex", plan: "pro" } }
      responses:
        '200':
          content:
            application/json:
              example: { status: "ok" }

  /v1/alias:
    post:
      summary: Связать два user_id
      description: Объединяет анонимный и авторизованный user_id.
      tags: [Ingestion]
      security: [{ ApiKeyAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [user_id, alias_user_id]
              properties:
                alias_user_id: { type: string, example: "anon_abc" }
                user_id: { type: string, example: "user_123" }
      responses:
        '200':
          content:
            application/json:
              example: { status: "ok" }

  /v1/replay/chunks:
    post:
      summary: Сохранить replay chunk
      description: |
        Приём session replay чанка от SDK. Поддерживается `Content-Encoding: gzip`, лимит одного чанка - `10 MB`.
      tags: [Replay]
      security: [{ ApiKeyAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [session_id, chunk_index, events]
              properties:
                session_id: { type: string, example: "sess_123" }
                project_id:
                  type: integer
                  description: "Может быть передан клиентом, но на сервере project_id берётся из API key."
                chunk_index: { type: integer, example: 0 }
                events:
                  description: RRWeb payload
                  oneOf:
                    - type: array
                      items: { type: object }
                    - type: object
                timestamp: { type: string, format: date-time }
                user_id: { type: string }
                device_id: { type: string }
                page_url: { type: string, example: "https://app.example.com/settings" }
                page_key: { type: string, example: "app.example.com/settings" }
                layout_key: { type: string, example: "desktop:1a2b3c4d" }
                viewport_bucket: { type: string, example: "desktop" }
                viewport_w: { type: integer, example: 1440 }
                viewport_h: { type: integer, example: 900 }
      responses:
        '200':
          content:
            application/json:
              example: { status: "ok", chunk_id: 123, chunk_index: 0 }

  /v1/heatmap/events:
    post:
      summary: Сохранить heatmap события
      description: |
        Приём click / scroll / attention событий для heatmaps. Лимит batch - `500` событий, размер тела запроса - до `2 MB`.
      tags: [Heatmaps]
      security: [{ ApiKeyAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [events]
              properties:
                events:
                  type: array
                  maxItems: 500
                  items:
                    type: object
                    required: [session_id, page_url, event_type]
                    properties:
                      session_id: { type: string }
                      page_url: { type: string }
                      event_type: { type: string, enum: [click, scroll, attention] }
                      x: { type: integer }
                      y: { type: integer }
                      scroll_depth: { type: integer }
                      selector: { type: string }
                      page_key: { type: string, example: "example.com/pricing" }
                      layout_key: { type: string, example: "desktop:1a2b3c4d" }
                      viewport_bucket: { type: string, example: "desktop" }
                      viewport_w: { type: integer }
                      viewport_h: { type: integer }
                      click_x_rel: { type: number, format: float }
                      click_y_rel: { type: number, format: float }
                      doc_height: { type: integer }
                      scroll_y: { type: integer }
                      attention_ms: { type: integer }
                      timestamp: { type: string, format: date-time }
      responses:
        '200':
          content:
            application/json:
              example: { status: "ok", accepted: 2 }

  /v1/heatmap/data:
    get:
      summary: Получить агрегированные heatmap данные
      description: Dashboard endpoint. Для `type=click` ответ содержит `viewport_w` и массив точек, для `scroll` и `attention` - агрегированные диапазоны.
      tags: [Heatmaps]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: project_id, in: query, required: true, schema: { type: integer } }
        - { name: page_url, in: query, required: true, schema: { type: string } }
        - { name: type, in: query, required: true, schema: { type: string, enum: [click, scroll, attention] } }
        - { name: from, in: query, required: false, schema: { type: string, format: date } }
        - { name: to, in: query, required: false, schema: { type: string, format: date } }
        - { name: segment_id, in: query, required: false, schema: { type: integer } }
      responses:
        '200':
          content:
            application/json:
              examples:
                click:
                  value:
                    type: "click"
                    page_key: "example.com/pricing"
                    layout_key: "desktop:1a2b3c4d"
                    viewport_bucket: "desktop"
                    viewport_w: 1440
                    viewport_h: 900
                    data:
                      - { x: 720, y: 450, count: 1830, selector: ".cta-button", click_x_rel: 0.5, click_y_rel: 0.45, uses_relative: true }
                scroll:
                  value:
                    type: "scroll"
                    page_key: "example.com/pricing"
                    layout_key: "desktop:1a2b3c4d"
                    viewport_bucket: "desktop"
                    viewport_w: 1440
                    viewport_h: 900
                    data:
                      - { depth_pct: 25, user_count: 4200, user_pct: 100 }
                      - { depth_pct: 50, user_count: 3100, user_pct: 73.8 }
                attention:
                  value:
                    type: "attention"
                    page_key: "example.com/pricing"
                    layout_key: "desktop:1a2b3c4d"
                    viewport_bucket: "desktop"
                    viewport_w: 1440
                    viewport_h: 900
                    data:
                      - { band: "0-10%", band_start: 0, band_end: 10, avg_ms: 3150, total_ms: 612000, session_count: 194, intensity: 0.82 }

  /v1/heatmap/canonical-replay:
    get:
      summary: Получить canonical replay snapshot для heatmap preview
      description: |
        Dashboard endpoint. Возвращает одну replay session и её chunks, наиболее подходящие под доминирующий layout для указанной страницы и периода.
        Используется heatmap UI как безопасная альтернатива HTML proxy и live screenshot.
      tags: [Heatmaps]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: project_id, in: query, required: true, schema: { type: integer } }
        - { name: page_url, in: query, required: true, schema: { type: string } }
        - { name: from, in: query, required: false, schema: { type: string, format: date } }
        - { name: to, in: query, required: false, schema: { type: string, format: date } }
        - { name: segment_id, in: query, required: false, schema: { type: integer } }
      responses:
        '200':
          content:
            application/json:
              example:
                variant:
                  page_key: "example.com/pricing"
                  layout_key: "desktop:1a2b3c4d"
                  viewport_bucket: "desktop"
                  viewport_w: 1440
                  viewport_h: 900
                session:
                  session_id: "sess_123"
                  page_url: "https://example.com/pricing"
                  page_key: "example.com/pricing"
                  layout_key: "desktop:1a2b3c4d"
                  viewport_bucket: "desktop"
                  viewport_w: 1440
                  viewport_h: 900
                chunks:
                  - chunk_index: 0
                    events: []

  /v1/heatmap/screenshot:
    get:
      summary: Получить PNG screenshot для heatmap overlay
      description: Dashboard endpoint. Делает screenshot страницы через отдельный screenshot service после валидации URL и проверки доступа к проекту.
      tags: [Heatmaps]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: project_id, in: query, required: true, schema: { type: integer } }
        - { name: url, in: query, required: true, schema: { type: string, format: uri } }
        - { name: width, in: query, required: false, schema: { type: integer, minimum: 320, maximum: 4096, default: 1440 } }
      responses:
        '200':
          description: PNG screenshot
          content:
            image/png:
              schema:
                type: string
                format: binary

  /v1/heatmap/proxy:
    get:
      summary: Legacy heatmap HTML proxy
      description: HTML proxy intentionally removed for security reasons. Endpoint kept only to return a deprecation error.
      deprecated: true
      tags: [Heatmaps]
      responses:
        '410': { description: "Gone. Use /v1/heatmap/screenshot" }

  # ═══════════════════════════════════════════════════════════════
  # Query
  # ═══════════════════════════════════════════════════════════════

  /v1/query/events:
    post:
      summary: Event Segmentation
      description: |
        Запрос метрик по событиям с группировкой и фильтрацией.

        ```bash
        curl -X POST http://localhost:8080/v1/query/events \
          -H "Authorization: Bearer <token>" \
          -d '{"project_id":1,"events":[{"event_name":"page_view"}],"date_range":{"start":"2026-03-01","end":"2026-03-15"},"metric":"total","interval":"day"}'
        ```
      tags: [Query]
      security: [{ BearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/QueryRequest' }
      responses:
        '200': { description: OK }
        '400': { description: Bad Request, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
        '401': { description: Unauthorized }

  /v1/query/funnels:
    post:
      summary: Funnel Analysis
      description: |
        ```bash
        curl -X POST http://localhost:8080/v1/query/funnels \
          -H "Authorization: Bearer <token>" \
          -d '{"project_id":1,"steps":[{"event_name":"page_view"},{"event_name":"signup_start"},{"event_name":"signup_complete"}],"date_range":{"start":"2026-03-01","end":"2026-03-15"}}'
        ```
      tags: [Query]
      security: [{ BearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/FunnelRequest' }
      responses:
        '200': { description: OK }
        '401': { description: Unauthorized }

  /v1/query/funnels/users:
    post:
      summary: Funnel Users
      description: Список пользователей, выпавших на определённом шаге воронки.
      tags: [Query]
      security: [{ BearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/FunnelRequest' }
      responses:
        '200': { description: OK }

  /v1/query/retention:
    post:
      summary: Retention Analysis
      description: |
        ```bash
        curl -X POST http://localhost:8080/v1/query/retention \
          -H "Authorization: Bearer <token>" \
          -d '{"project_id":1,"start_event":"signup_complete","return_event":"page_view","date_range":{"start":"2026-02-01","end":"2026-03-15"},"granularity":"week"}'
        ```
      tags: [Query]
      security: [{ BearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/RetentionRequest' }
      responses:
        '200': { description: OK }

  /v1/query/retention/users:
    post:
      summary: Retention Users
      tags: [Query]
      security: [{ BearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/RetentionRequest' }
      responses:
        '200': { description: OK }

  /v1/query/retention/compare:
    post:
      summary: Retention Compare
      description: Сравнение retention по сегментам или когортам.
      tags: [Query]
      security: [{ BearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/RetentionRequest' }
      responses:
        '200': { description: OK }

  /v1/query/journeys:
    post:
      summary: User Journeys
      description: |
        ```bash
        curl -X POST http://localhost:8080/v1/query/journeys \
          -H "Authorization: Bearer <token>" \
          -d '{"project_id":1,"start_event":"signup_complete","date_range":{"start":"2026-03-01","end":"2026-03-15"},"max_steps":5}'
        ```
      tags: [Query]
      security: [{ BearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/JourneyRequest' }
      responses:
        '200': { description: OK }

  /v1/query/journeys/users:
    post:
      summary: Journey Users
      tags: [Query]
      security: [{ BearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/JourneyRequest' }
      responses:
        '200': { description: OK }

  /v1/query/journeys/compare:
    post:
      summary: Journey Compare
      tags: [Query]
      security: [{ BearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/JourneyRequest' }
      responses:
        '200': { description: OK }

  /v1/query/revenue:
    post:
      summary: Revenue Analysis
      tags: [Query]
      security: [{ BearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [project_id, date_range]
              properties:
                project_id: { type: integer }
                date_range:
                  type: object
                  properties:
                    start: { type: string, format: date }
                    end: { type: string, format: date }
      responses:
        '200': { description: OK }

  /v1/query/revenue/overview:
    post:
      summary: Revenue Overview
      tags: [Query]
      security: [{ BearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [project_id, date_range]
              properties:
                project_id: { type: integer }
                date_range:
                  type: object
                  properties:
                    start: { type: string, format: date }
                    end: { type: string, format: date }
      responses:
        '200': { description: OK }

  /v1/query/revenue/ltv:
    post:
      summary: Lifetime Value
      tags: [Query]
      security: [{ BearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [project_id, date_range]
              properties:
                project_id: { type: integer }
                date_range:
                  type: object
                  properties:
                    start: { type: string, format: date }
                    end: { type: string, format: date }
      responses:
        '200': { description: OK }

  /v1/query/revenue/ltv/drilldown:
    post:
      summary: LTV Drilldown
      tags: [Query]
      security: [{ BearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [project_id, date_range]
              properties:
                project_id: { type: integer }
                date_range:
                  type: object
                  properties:
                    start: { type: string, format: date }
                    end: { type: string, format: date }
      responses:
        '200': { description: OK }

  /v1/query/revenue/top-payers:
    post:
      summary: Top Payers
      tags: [Query]
      security: [{ BearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [project_id, date_range]
              properties:
                project_id: { type: integer }
                date_range:
                  type: object
                  properties:
                    start: { type: string, format: date }
                    end: { type: string, format: date }
      responses:
        '200': { description: OK }

  /v1/query/experiments/results:
    post:
      summary: Experiment Results
      description: Рассчитывает статистическую значимость A/B-теста.
      tags: [Experiments]
      security: [{ BearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [project_id, experiment_key, metric_event, date_from, date_to]
              properties:
                project_id: { type: integer }
                experiment_key: { type: string }
                metric_event: { type: string }
                metric_type: { type: string, enum: [conversion, revenue] }
                method: { type: string, enum: [standard, cuped, sequential, sequential_cuped] }
                date_from: { type: string, format: date }
                date_to: { type: string, format: date }
                confidence_level: { type: number, default: 0.95 }
                experiment_id: { type: integer }
                min_detect_effect: { type: number }
      responses:
        '200': { description: OK }

  /v1/query/experiments/sample-size:
    post:
      summary: Sample Size Calculator
      tags: [Experiments]
      security: [{ BearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [baseline_rate, min_effect]
              properties:
                baseline_rate: { type: number, example: 0.05 }
                min_effect: { type: number, example: 0.1 }
                significance: { type: number, default: 0.05 }
                power: { type: number, default: 0.8 }
                variants: { type: integer, default: 2 }
      responses:
        '200':
          content:
            application/json:
              example: { sample_per_variant: 3000, total_sample: 6000, estimated_days: 14 }

  /v1/query/experiments/{id}/sequential-history:
    get:
      summary: Sequential Test History
      tags: [Experiments]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK }

  # ═══════════════════════════════════════════════════════════════
  # Segments
  # ═══════════════════════════════════════════════════════════════

  /v1/segments:
    get:
      summary: Список сегментов
      tags: [Segments]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: project_id, in: query, required: true, schema: { type: integer } }
      responses:
        '200':
          content:
            application/json:
              example: { segments: [] }
    post:
      summary: Создать сегмент
      description: |
        ```bash
        curl -X POST http://localhost:8080/v1/segments \
          -H "Authorization: Bearer <token>" \
          -d '{"project_id":1,"name":"Active Users","type":"behavioral","rules":{"events":[{"event_name":"page_view","count":{"gte":5}}]},"is_dynamic":true}'
        ```
      tags: [Segments]
      security: [{ BearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [project_id, name, type]
              properties:
                project_id: { type: integer }
                name: { type: string }
                description: { type: string }
                type: { type: string, enum: [behavioral, property, combined] }
                rules: { type: object }
                is_dynamic: { type: boolean, default: false }
      responses:
        '201': { description: Created }

  /v1/segments/{id}:
    get:
      summary: Получить сегмент
      tags: [Segments]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK }
    put:
      summary: Обновить сегмент
      tags: [Segments]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string }
                description: { type: string }
                type: { type: string }
                rules: { type: object }
                is_dynamic: { type: boolean }
      responses:
        '200': { description: OK }
    delete:
      summary: Удалить сегмент
      tags: [Segments]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200':
          content:
            application/json:
              example: { status: "deleted" }

  /v1/segments/preview:
    post:
      summary: Предпросмотр сегмента
      description: Возвращает количество пользователей, попадающих в сегмент.
      tags: [Segments]
      security: [{ BearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [project_id, rules]
              properties:
                project_id: { type: integer }
                rules: { type: object }
      responses:
        '200':
          content:
            application/json:
              example: { count: 1523 }

  # ═══════════════════════════════════════════════════════════════
  # Dashboards
  # ═══════════════════════════════════════════════════════════════

  /v1/dashboards:
    get:
      summary: Список дашбордов
      tags: [Dashboards]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: project_id, in: query, required: true, schema: { type: integer } }
      responses:
        '200':
          content:
            application/json:
              example: { dashboards: [], limit: 5 }
    post:
      summary: Создать дашборд
      tags: [Dashboards]
      security: [{ BearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [project_id, name]
              properties:
                project_id: { type: integer }
                name: { type: string, example: "KPI Overview" }
                description: { type: string }
      responses:
        '201': { description: Created }

  /v1/dashboards/{id}:
    get:
      summary: Получить дашборд
      tags: [Dashboards]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK }
    put:
      summary: Обновить дашборд
      tags: [Dashboards]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string }
                description: { type: string }
                layout: { type: object }
      responses:
        '200': { description: OK }
    delete:
      summary: Удалить дашборд
      tags: [Dashboards]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200':
          content:
            application/json:
              example: { status: "deleted" }

  /v1/dashboards/{id}/share:
    post:
      summary: Включить публичный доступ
      tags: [Dashboards]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200':
          content:
            application/json:
              example: { share_token: "abc123def..." }
    delete:
      summary: Отключить публичный доступ
      tags: [Dashboards]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200':
          content:
            application/json:
              example: { status: "unshared" }

  /v1/dashboards/{id}/widgets:
    get:
      summary: Виджеты дашборда
      tags: [Dashboards]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200':
          content:
            application/json:
              example: { widgets: [] }
    post:
      summary: Создать виджет
      tags: [Dashboards]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [widget_type, position]
              properties:
                report_id: { type: integer, nullable: true }
                widget_type: { type: string, enum: [chart, number, table, funnel, retention, text] }
                position: { type: object, description: "{x, y, w, h}" }
                config: { type: object }
      responses:
        '201': { description: Created }

  /v1/dashboards/{dashboardId}/widgets/{widgetId}:
    put:
      summary: Обновить виджет
      tags: [Dashboards]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: dashboardId, in: path, required: true, schema: { type: integer } }
        - { name: widgetId, in: path, required: true, schema: { type: integer } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                widget_type: { type: string }
                position: { type: object }
                config: { type: object }
      responses:
        '200': { description: OK }
    delete:
      summary: Удалить виджет
      tags: [Dashboards]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: dashboardId, in: path, required: true, schema: { type: integer } }
        - { name: widgetId, in: path, required: true, schema: { type: integer } }
      responses:
        '200':
          content:
            application/json:
              example: { status: "deleted" }

  /v1/public/dashboards/{token}:
    get:
      summary: Публичный дашборд
      description: Доступ без авторизации по share token.
      tags: [Dashboards]
      parameters:
        - { name: token, in: path, required: true, schema: { type: string } }
      responses:
        '200': { description: OK }
        '404': { description: Not found }

  /v1/dashboards/{id}/subscriptions:
    get:
      summary: Подписки на дашборд
      tags: [Dashboards]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK }
    post:
      summary: Создать подписку
      description: Email-рассылка дашборда по расписанию.
      tags: [Dashboards]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [frequency, recipients]
              properties:
                frequency: { type: string, enum: [daily, weekly, monthly] }
                recipients: { type: array, items: { type: string, format: email } }
      responses:
        '201': { description: Created }

  # ═══════════════════════════════════════════════════════════════
  # Alerts
  # ═══════════════════════════════════════════════════════════════

  /v1/alerts:
    get:
      summary: Список алертов
      tags: [Alerts]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: project_id, in: query, required: true, schema: { type: integer } }
      responses:
        '200':
          content:
            application/json:
              example: { alerts: [] }
    post:
      summary: Создать алерт
      description: |
        ```bash
        curl -X POST http://localhost:8080/v1/alerts \
          -H "Authorization: Bearer <token>" \
          -d '{"project_id":1,"name":"Drop in signups","type":"threshold","metric":"total","event_name":"signup_complete","condition":"lt","threshold":10,"window_minutes":60,"channels":[{"type":"email","target":"alex@example.com"},{"type":"slack","target":"#alerts"}],"cooldown_min":120}'
        ```
      tags: [Alerts]
      security: [{ BearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [project_id, name, condition]
              properties:
                project_id: { type: integer }
                name: { type: string }
                type: { type: string, enum: [threshold, anomaly, segment_size], default: threshold }
                metric: { type: string, default: total }
                event_name: { type: string }
                segment_id: { type: integer }
                condition: { type: string, enum: [gt, gte, lt, lte, eq] }
                threshold: { type: number }
                window_minutes: { type: integer, default: 60 }
                channels:
                  type: array
                  items: { $ref: '#/components/schemas/AlertChannel' }
                cooldown_min: { type: integer, default: 60 }
      responses:
        '201':
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Alert' }

  /v1/alerts/{id}:
    get:
      summary: Получить алерт
      tags: [Alerts]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK }
    put:
      summary: Обновить алерт
      tags: [Alerts]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string }
                type: { type: string }
                metric: { type: string }
                event_name: { type: string }
                condition: { type: string }
                threshold: { type: number }
                window_minutes: { type: integer }
                channels: { type: array, items: { $ref: '#/components/schemas/AlertChannel' } }
                cooldown_min: { type: integer }
                is_active: { type: boolean }
      responses:
        '200':
          content:
            application/json:
              example: { status: "updated" }
    delete:
      summary: Удалить алерт
      tags: [Alerts]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200':
          content:
            application/json:
              example: { status: "deleted" }

  /v1/alerts/{id}/pause:
    post:
      summary: Поставить на паузу
      tags: [Alerts]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200':
          content:
            application/json:
              example: { status: "paused" }

  /v1/alerts/{id}/resume:
    post:
      summary: Возобновить
      tags: [Alerts]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200':
          content:
            application/json:
              example: { status: "active" }

  /v1/alerts/{id}/triggers:
    get:
      summary: История срабатываний
      tags: [Alerts]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
        - { name: limit, in: query, schema: { type: integer, default: 20 } }
        - { name: offset, in: query, schema: { type: integer, default: 0 } }
      responses:
        '200':
          content:
            application/json:
              example: { triggers: [] }

  /v1/alerts/{id}/test:
    post:
      summary: Тестовая отправка
      tags: [Alerts]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200':
          content:
            application/json:
              example: { status: "sent" }

  # ═══════════════════════════════════════════════════════════════
  # Experiments
  # ═══════════════════════════════════════════════════════════════

  /v1/experiments:
    get:
      summary: Список экспериментов
      tags: [Experiments]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: project_id, in: query, required: true, schema: { type: integer } }
      responses:
        '200':
          content:
            application/json:
              example: { experiments: [] }
    post:
      summary: Создать эксперимент
      description: |
        ```bash
        curl -X POST http://localhost:8080/v1/experiments \
          -H "Authorization: Bearer <token>" \
          -d '{"project_id":1,"key":"new-checkout","name":"New Checkout Flow","primary_metric":"purchase","experiment_type":"ab_test","variants":[{"key":"control","name":"Control","weight":50,"is_control":true},{"key":"treatment","name":"Treatment","weight":50}]}'
        ```
      tags: [Experiments]
      security: [{ BearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [project_id, key, name, primary_metric, variants]
              properties:
                project_id: { type: integer }
                key: { type: string }
                name: { type: string }
                description: { type: string }
                hypothesis: { type: string }
                traffic_pct: { type: integer, default: 100 }
                primary_metric: { type: string }
                secondary_metrics: { type: array, items: { type: string } }
                confidence_level: { type: number, default: 0.95 }
                statistics_method: { type: string, enum: [fixed, sequential], default: fixed }
                experiment_type: { type: string, enum: [ab_test, bandit], default: ab_test }
                variants:
                  type: array
                  minItems: 2
                  items:
                    type: object
                    required: [key, name, weight]
                    properties:
                      key: { type: string }
                      name: { type: string }
                      weight: { type: integer }
                      is_control: { type: boolean, default: false }
      responses:
        '201':
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Experiment' }

  /v1/experiments/{id}:
    get:
      summary: Получить эксперимент
      tags: [Experiments]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK }
    put:
      summary: Обновить эксперимент
      tags: [Experiments]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string }
                description: { type: string }
                hypothesis: { type: string }
                traffic_pct: { type: integer }
                primary_metric: { type: string }
                confidence_level: { type: number }
      responses:
        '200':
          content:
            application/json:
              example: { status: "updated" }
    delete:
      summary: Удалить эксперимент
      tags: [Experiments]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200':
          content:
            application/json:
              example: { status: "deleted" }

  /v1/experiments/{id}/start:
    post:
      summary: Запустить эксперимент
      tags: [Experiments]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200':
          content:
            application/json:
              example: { status: "running" }

  /v1/experiments/{id}/stop:
    post:
      summary: Остановить эксперимент
      tags: [Experiments]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200':
          content:
            application/json:
              example: { status: "paused" }

  /v1/experiments/{id}/complete:
    post:
      summary: Завершить эксперимент
      tags: [Experiments]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200':
          content:
            application/json:
              example: { status: "completed" }

  /v1/experiments/{id}/assign:
    get:
      summary: Назначить вариант пользователю
      description: Возвращает назначенный вариант для user_id. Используется SDK.
      tags: [Experiments]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
        - { name: user_id, in: query, required: true, schema: { type: string } }
      responses:
        '200':
          content:
            application/json:
              example: { variant_key: "treatment", algorithm: "sticky_hash" }

  /v1/experiments/{id}/switch-strategy:
    post:
      summary: Переключить стратегию
      description: Переключение между ab_test и bandit.
      tags: [Experiments]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [experiment_type]
              properties:
                experiment_type: { type: string, enum: [ab_test, bandit] }
      responses:
        '200':
          content:
            application/json:
              example: { status: "switched", experiment_type: "bandit" }

  /v1/experiments/{id}/bandit-state:
    get:
      summary: Bandit state
      tags: [Experiments]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK }

  /v1/experiments/{id}/bandit-history:
    get:
      summary: Bandit allocation history
      tags: [Experiments]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK }

  # ═══════════════════════════════════════════════════════════════
  # Feature Flags
  # ═══════════════════════════════════════════════════════════════

  /v1/flags:
    get:
      summary: Список флагов
      tags: [Feature Flags]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: project_id, in: query, required: true, schema: { type: integer } }
      responses:
        '200':
          content:
            application/json:
              example: { flags: [] }
    post:
      summary: Создать флаг
      description: |
        ```bash
        curl -X POST http://localhost:8080/v1/flags \
          -H "Authorization: Bearer <token>" \
          -d '{"project_id":1,"key":"new-checkout","name":"New Checkout Flow","type":"boolean"}'
        ```
      tags: [Feature Flags]
      security: [{ BearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [project_id, key, name]
              properties:
                project_id: { type: integer }
                key: { type: string, example: "new-checkout" }
                name: { type: string, example: "New Checkout Flow" }
                description: { type: string }
                type: { type: string, enum: [boolean, multivariate, percentage], default: boolean }
                default_value: {}
                targeting_rules: { type: object }
                variants: { type: array, items: { type: object } }
      responses:
        '201': { description: Created }

  /v1/flags/{id}:
    get:
      summary: Получить флаг
      tags: [Feature Flags]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK }
    put:
      summary: Обновить флаг
      tags: [Feature Flags]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string }
                description: { type: string }
                is_active: { type: boolean }
                rollout_pct: { type: integer }
                default_value: {}
                targeting_rules: { type: object }
                variants: { type: array, items: { type: object } }
      responses:
        '200':
          content:
            application/json:
              example: { status: "updated" }
    delete:
      summary: Удалить флаг
      tags: [Feature Flags]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200':
          content:
            application/json:
              example: { status: "deleted" }

  /v1/flags/{id}/toggle:
    post:
      summary: Переключить активность
      tags: [Feature Flags]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200':
          content:
            application/json:
              example: { is_active: true }

  /v1/flags/{id}/kill:
    post:
      summary: Kill Switch
      description: Мгновенно деактивирует флаг.
      tags: [Feature Flags]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200':
          content:
            application/json:
              example: { status: "killed" }

  /v1/flags/{id}/history:
    get:
      summary: История изменений флага
      tags: [Feature Flags]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200':
          content:
            application/json:
              example: { history: [] }

  /v1/flags/evaluate:
    post:
      summary: Evaluate Flag
      description: |
        SDK endpoint — без авторизации. Возвращает значение флага для пользователя.

        ```bash
        curl -X POST http://localhost:8080/v1/flags/evaluate \
          -d '{"project_id":1,"flag_key":"new-checkout","user_id":"user_123"}'
        ```
      tags: [Feature Flags]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [project_id, flag_key]
              properties:
                project_id: { type: integer }
                flag_key: { type: string }
                user_id: { type: string }
                properties:
                  type: object
                  additionalProperties: { type: string }
      responses:
        '200':
          content:
            application/json:
              example: { flag_key: "new-checkout", value: true, variant: "" }

  # ═══════════════════════════════════════════════════════════════
  # Rollout Plans
  # ═══════════════════════════════════════════════════════════════

  /v1/rollout-plans:
    get:
      summary: Список планов раскатки
      tags: [Rollout Plans]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: project_id, in: query, required: true, schema: { type: integer } }
      responses:
        '200':
          content:
            application/json:
              example: { plans: [] }
    post:
      summary: Создать план раскатки
      description: |
        Stages должны идти по возрастанию percentage.

        ```bash
        curl -X POST http://localhost:8080/v1/rollout-plans \
          -H "Authorization: Bearer <token>" \
          -d '{"flag_id":1,"project_id":1,"stages":[{"percentage":10,"delay_hours":24},{"percentage":50,"delay_hours":24},{"percentage":100,"delay_hours":0}]}'
        ```
      tags: [Rollout Plans]
      security: [{ BearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [flag_id, project_id, stages]
              properties:
                flag_id: { type: integer }
                project_id: { type: integer }
                stages:
                  type: array
                  items:
                    type: object
                    required: [percentage, delay_hours]
                    properties:
                      percentage: { type: integer, minimum: 0, maximum: 100 }
                      delay_hours: { type: integer }
                pause_on_alert_id: { type: integer }
      responses:
        '201': { description: Created }

  /v1/rollout-plans/{id}:
    get:
      summary: Получить план
      tags: [Rollout Plans]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK }
    delete:
      summary: Удалить план
      tags: [Rollout Plans]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200':
          content:
            application/json:
              example: { status: "deleted" }

  /v1/rollout-plans/{id}/pause:
    post:
      summary: Пауза раскатки
      tags: [Rollout Plans]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200':
          content:
            application/json:
              example: { status: "paused" }

  /v1/rollout-plans/{id}/resume:
    post:
      summary: Возобновить раскатку
      tags: [Rollout Plans]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200':
          content:
            application/json:
              example: { status: "active" }

  /v1/rollout-plans/{id}/rollback:
    post:
      summary: Откатить раскатку
      tags: [Rollout Plans]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200':
          content:
            application/json:
              example: { status: "rolled_back", current_stage: 0, rollout_pct: 0 }

  # ═══════════════════════════════════════════════════════════════
  # Mutex Groups
  # ═══════════════════════════════════════════════════════════════

  /v1/mutex-groups:
    get:
      summary: Список взаимоисключающих групп
      tags: [Mutex Groups]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: project_id, in: query, required: true, schema: { type: integer } }
      responses:
        '200':
          content:
            application/json:
              example: { groups: [] }
    post:
      summary: Создать группу
      tags: [Mutex Groups]
      security: [{ BearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [project_id, name]
              properties:
                project_id: { type: integer }
                name: { type: string }
                description: { type: string }
      responses:
        '201': { description: Created }

  /v1/mutex-groups/{id}:
    get:
      summary: Получить группу
      tags: [Mutex Groups]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK }
    put:
      summary: Обновить группу
      tags: [Mutex Groups]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string }
                description: { type: string }
      responses:
        '200': { description: OK }
    delete:
      summary: Удалить группу
      tags: [Mutex Groups]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200':
          content:
            application/json:
              example: { status: "deleted" }

  /v1/mutex-groups/{id}/members:
    post:
      summary: Добавить эксперимент в группу
      tags: [Mutex Groups]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [experiment_id]
              properties:
                experiment_id: { type: integer }
      responses:
        '200': { description: OK }

  /v1/mutex-groups/{id}/members/{experimentId}:
    delete:
      summary: Удалить эксперимент из группы
      tags: [Mutex Groups]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
        - { name: experimentId, in: path, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK }

  /v1/mutex-groups/check:
    get:
      summary: Проверить bucket
      description: SDK-endpoint для проверки, в какой эксперимент попадает пользователь.
      tags: [Mutex Groups]
      parameters:
        - { name: project_id, in: query, required: true, schema: { type: integer } }
        - { name: group_id, in: query, required: true, schema: { type: integer } }
        - { name: user_id, in: query, required: true, schema: { type: string } }
      responses:
        '200': { description: OK }

  # ═══════════════════════════════════════════════════════════════
  # Export
  # ═══════════════════════════════════════════════════════════════

  /v1/export/events:
    post:
      summary: Экспорт событий
      description: |
        Поддерживает курсорную пагинацию. Форматы: ndjson, csv, parquet.

        ```bash
        curl -X POST http://localhost:8080/v1/export/events \
          -H "Authorization: Bearer <token>" \
          -d '{"project_id":1,"date_range":{"start":"2026-03-01","end":"2026-03-15"},"format":"csv","limit":1000}'
        ```
      tags: [Export]
      security: [{ BearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [project_id, date_range]
              properties:
                project_id: { type: integer }
                date_range:
                  type: object
                  properties:
                    start: { type: string, format: date }
                    end: { type: string, format: date }
                event_types: { type: array, items: { type: string } }
                filters: { type: object }
                format: { type: string, enum: [ndjson, csv, parquet], default: ndjson }
                cursor: { type: string }
                limit: { type: integer, default: 10000 }
      responses:
        '200': { description: OK }

  /v1/export/users:
    post:
      summary: Экспорт пользователей
      tags: [Export]
      security: [{ BearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [project_id]
              properties:
                project_id: { type: integer }
                filters: { type: object }
                properties: { type: array, items: { type: string } }
                format: { type: string, default: ndjson }
                cursor: { type: string }
                limit: { type: integer }
      responses:
        '200': { description: OK }

  /v1/export/cohorts/{id}/users:
    get:
      summary: Экспорт пользователей когорты
      tags: [Export]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
        - { name: project_id, in: query, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK }

  /v1/export/jobs/{id}:
    get:
      summary: Статус задачи экспорта
      tags: [Export]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200':
          content:
            application/json:
              example: { id: 1, status: "completed", type: "events", format: "csv", rows_exported: 5000, download_url: "/v1/export/download/1" }

  /v1/export/download/{id}:
    get:
      summary: Скачать экспорт
      tags: [Export]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200':
          description: File download
          headers:
            Content-Disposition: { schema: { type: string } }

  /v1/export/jobs:
    get:
      summary: Список задач экспорта
      tags: [Export]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: project_id, in: query, required: true, schema: { type: integer } }
        - { name: limit, in: query, schema: { type: integer, default: 20 } }
        - { name: offset, in: query, schema: { type: integer, default: 0 } }
      responses:
        '200':
          content:
            application/json:
              example: { jobs: [] }

  /v1/export/schedules:
    get:
      summary: Список расписаний экспорта
      tags: [Export]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: project_id, in: query, required: true, schema: { type: integer } }
      responses:
        '200':
          content:
            application/json:
              example: { schedules: [] }
    post:
      summary: Создать расписание экспорта
      tags: [Export]
      security: [{ BearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [project_id, type, frequency, destination]
              properties:
                project_id: { type: integer }
                type: { type: string }
                filters: { type: object }
                format: { type: string, default: ndjson }
                frequency: { type: string, enum: [daily, weekly, monthly] }
                destination: { type: object }
      responses:
        '201': { description: Created }

  /v1/export/schedules/{id}:
    delete:
      summary: Удалить расписание
      tags: [Export]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
        - { name: project_id, in: query, required: true, schema: { type: integer } }
      responses:
        '200':
          content:
            application/json:
              example: { status: "deleted" }

  # ═══════════════════════════════════════════════════════════════
  # Projects
  # ═══════════════════════════════════════════════════════════════

  /v1/projects:
    get:
      summary: Список проектов
      tags: [Projects]
      security: [{ BearerAuth: [] }]
      responses:
        '200': { description: OK }
    post:
      summary: Создать проект
      tags: [Projects]
      security: [{ BearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string, example: "My App" }
                timezone: { type: string, default: "Europe/Moscow" }
      responses:
        '201': { description: Created }

  /v1/projects/{id}:
    get:
      summary: Получить проект
      tags: [Projects]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK }
    put:
      summary: Обновить проект
      tags: [Projects]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string }
                timezone: { type: string }
      responses:
        '200': { description: OK }
    delete:
      summary: Удалить проект
      tags: [Projects]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200':
          content:
            application/json:
              example: { status: "deleted" }

  /v1/projects/{id}/keys/rotate:
    post:
      summary: Ротация ключей проекта
      tags: [Projects]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK }

  # ═══════════════════════════════════════════════════════════════
  # API Keys
  # ═══════════════════════════════════════════════════════════════

  /v1/api-keys:
    get:
      summary: Список API-ключей
      tags: [API Keys]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: project_id, in: query, required: true, schema: { type: integer } }
      responses:
        '200':
          content:
            application/json:
              example: { keys: [] }
    post:
      summary: Создать API-ключ
      tags: [API Keys]
      security: [{ BearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [project_id, name, type]
              properties:
                project_id: { type: integer }
                name: { type: string }
                type: { type: string, enum: [secret, public] }
                scopes: { type: array, items: { type: string, enum: [query, read, write, admin] } }
                rate_limit: { type: integer, description: "Requests per minute" }
      responses:
        '201':
          description: "Key is only shown once"
          content:
            application/json:
              example: { id: 1, key: "sk_abc123...", name: "Production", scopes: ["query", "read"] }

  /v1/api-keys/{id}:
    delete:
      summary: Отозвать API-ключ
      tags: [API Keys]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200':
          content:
            application/json:
              example: { status: "revoked" }

  # ═══════════════════════════════════════════════════════════════
  # Webhooks
  # ═══════════════════════════════════════════════════════════════

  /v1/webhooks:
    get:
      summary: Список вебхуков
      tags: [Webhooks]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: project_id, in: query, required: true, schema: { type: integer } }
      responses:
        '200':
          content:
            application/json:
              example: { webhooks: [] }
    post:
      summary: Создать вебхук
      tags: [Webhooks]
      security: [{ BearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [project_id, url, events]
              properties:
                project_id: { type: integer }
                url: { type: string, format: uri }
                events: { type: array, items: { type: string }, description: "Events: alert_triggered, experiment_completed, etc." }
                secret: { type: string }
      responses:
        '201': { description: Created }

  /v1/webhooks/{id}:
    put:
      summary: Обновить вебхук
      tags: [Webhooks]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                url: { type: string }
                events: { type: array, items: { type: string } }
                is_active: { type: boolean }
      responses:
        '200': { description: OK }
    delete:
      summary: Удалить вебхук
      tags: [Webhooks]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200':
          content:
            application/json:
              example: { status: "deleted" }

  /v1/webhooks/{id}/test:
    post:
      summary: Тестовая отправка
      tags: [Webhooks]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200':
          content:
            application/json:
              example: { status: "sent" }

  /v1/webhooks/{id}/logs:
    get:
      summary: Логи вебхука
      tags: [Webhooks]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK }

  # ═══════════════════════════════════════════════════════════════
  # Integrations
  # ═══════════════════════════════════════════════════════════════

  /v1/telegram/status:
    get:
      summary: Статус Telegram подключения
      tags: [Integrations]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: project_id, in: query, required: true, schema: { type: integer } }
      responses:
        '200':
          content:
            application/json:
              examples:
                connected: { value: { connected: true, chat_id: 123456, connected_at: "2026-03-15T10:00:00Z" } }
                pending: { value: { connected: false, pending: true, link: "https://t.me/TrektikBot?start=abc..." } }
                none: { value: { connected: false } }

  /v1/telegram/connect:
    post:
      summary: Подключить Telegram
      description: Генерирует deeplink для подключения бота.
      tags: [Integrations]
      security: [{ BearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [project_id]
              properties:
                project_id: { type: integer }
      responses:
        '200':
          content:
            application/json:
              example: { link: "https://t.me/TrektikBot?start=abc...", token: "abc..." }

  /v1/telegram/disconnect:
    post:
      summary: Отключить Telegram
      tags: [Integrations]
      security: [{ BearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [project_id]
              properties:
                project_id: { type: integer }
      responses:
        '200':
          content:
            application/json:
              example: { status: "disconnected" }

  /v1/slack/connections:
    get:
      summary: Список Slack подключений
      tags: [Integrations]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: project_id, in: query, required: true, schema: { type: integer } }
      responses:
        '200':
          content:
            application/json:
              example: { connections: [] }

  /v1/slack/connect:
    post:
      summary: Подключить Slack канал
      description: Добавляет Incoming Webhook URL для уведомлений.
      tags: [Integrations]
      security: [{ BearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [project_id, webhook_url]
              properties:
                project_id: { type: integer }
                webhook_url: { type: string, format: uri, example: "https://hooks.slack.com/services/..." }
                channel_id: { type: string }
                channel_name: { type: string, example: "#alerts" }
                team_id: { type: string }
                team_name: { type: string }
      responses:
        '201': { description: Created }

  /v1/slack/connections/{id}:
    delete:
      summary: Отключить Slack канал
      tags: [Integrations]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
        - { name: project_id, in: query, required: true, schema: { type: integer } }
      responses:
        '200':
          content:
            application/json:
              example: { status: "disconnected" }

  /v1/slack/test:
    post:
      summary: Тестовое Slack сообщение
      tags: [Integrations]
      security: [{ BearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [webhook_url]
              properties:
                webhook_url: { type: string, format: uri }
                project_id: { type: integer }
      responses:
        '200':
          content:
            application/json:
              example: { status: "ok" }

  /v1/kafka-integrations:
    get:
      summary: Список Kafka интеграций
      tags: [Integrations]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: project_id, in: query, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK }
    post:
      summary: Создать Kafka интеграцию
      tags: [Integrations]
      security: [{ BearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [project_id, bootstrap_servers, topic]
              properties:
                project_id: { type: integer }
                bootstrap_servers: { type: string, example: "kafka:9092" }
                topic: { type: string, example: "events" }
                sasl_mechanism: { type: string, enum: ["", PLAIN, SCRAM-SHA-256, SCRAM-SHA-512] }
                sasl_username: { type: string }
                sasl_password: { type: string }
                format: { type: string, enum: [json, avro], default: json }
                event_filter: { type: array, items: { type: string } }
      responses:
        '201': { description: Created }

  /v1/kafka-integrations/{id}:
    get:
      summary: Получить Kafka интеграцию
      tags: [Integrations]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK }
    put:
      summary: Обновить Kafka интеграцию
      tags: [Integrations]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                bootstrap_servers: { type: string }
                topic: { type: string }
                sasl_mechanism: { type: string }
                sasl_username: { type: string }
                sasl_password: { type: string }
                format: { type: string }
                event_filter: { type: array, items: { type: string } }
                is_active: { type: boolean }
      responses:
        '200': { description: OK }
    delete:
      summary: Удалить Kafka интеграцию
      tags: [Integrations]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200':
          content:
            application/json:
              example: { status: "deleted" }

  /v1/kafka-integrations/test:
    post:
      summary: Тест Kafka подключения
      tags: [Integrations]
      security: [{ BearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [bootstrap_servers, topic]
              properties:
                bootstrap_servers: { type: string }
                topic: { type: string }
                sasl_mechanism: { type: string }
                sasl_username: { type: string }
                sasl_password: { type: string }
      responses:
        '200':
          content:
            application/json:
              example: { status: "ok" }

  # ═══════════════════════════════════════════════════════════════
  # Taxonomy (Tracking Plan)
  # ═══════════════════════════════════════════════════════════════

  /v1/taxonomy/events:
    get:
      summary: Список событий Tracking Plan
      tags: [Taxonomy]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: project_id, in: query, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK }
    post:
      summary: Создать определение события
      tags: [Taxonomy]
      security: [{ BearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [project_id, event_name]
              properties:
                project_id: { type: integer }
                event_name: { type: string }
                display_name: { type: string }
                category: { type: string }
                description: { type: string }
                status: { type: string, enum: [planned, live, blocked, deprecated] }
      responses:
        '201': { description: Created }

  /v1/taxonomy/events/{id}:
    get:
      summary: Получить событие
      tags: [Taxonomy]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK }
    put:
      summary: Обновить событие
      tags: [Taxonomy]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK }
    delete:
      summary: Удалить событие
      tags: [Taxonomy]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK }

  /v1/taxonomy/import:
    post:
      summary: Импорт Tracking Plan (JSON)
      tags: [Taxonomy]
      security: [{ BearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { type: object }
      responses:
        '200': { description: OK }

  /v1/taxonomy/export:
    get:
      summary: Экспорт Tracking Plan (JSON)
      tags: [Taxonomy]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: project_id, in: query, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK }

  /v1/taxonomy/settings:
    get:
      summary: Настройки Tracking Plan
      tags: [Taxonomy]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: project_id, in: query, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK }
    put:
      summary: Обновить настройки
      tags: [Taxonomy]
      security: [{ BearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { type: object }
      responses:
        '200': { description: OK }

  /v1/taxonomy/history:
    get:
      summary: История изменений Tracking Plan
      tags: [Taxonomy]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: project_id, in: query, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK }

  # ═══════════════════════════════════════════════════════════════
  # Data Quality & Users
  # ═══════════════════════════════════════════════════════════════

  /v1/data-quality:
    get:
      summary: Data Quality
      description: Анализ качества данных — дубликаты, частота событий, аномалии.
      tags: [Data Quality]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: project_id, in: query, required: true, schema: { type: integer } }
        - { name: start, in: query, schema: { type: string, format: date } }
        - { name: end, in: query, schema: { type: string, format: date } }
        - { name: limit, in: query, schema: { type: integer, default: 100 } }
        - { name: offset, in: query, schema: { type: integer, default: 0 } }
      responses:
        '200': { description: OK }

  /v1/users/search:
    get:
      summary: Поиск пользователей
      tags: [Users]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: project_id, in: query, required: true, schema: { type: integer } }
        - { name: q, in: query, schema: { type: string } }
        - { name: limit, in: query, schema: { type: integer, default: 20 } }
      responses:
        '200': { description: OK }

  /v1/users/{id}:
    get:
      summary: Профиль пользователя
      tags: [Users]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
        - { name: project_id, in: query, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK }

  /v1/users/{id}/events:
    get:
      summary: События пользователя
      tags: [Users]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
        - { name: project_id, in: query, required: true, schema: { type: integer } }
        - { name: limit, in: query, schema: { type: integer, default: 50 } }
      responses:
        '200': { description: OK }

  # ═══════════════════════════════════════════════════════════════
  # Usage
  # ═══════════════════════════════════════════════════════════════

  /v1/usage:
    get:
      summary: Лимиты и потребление
      tags: [Usage]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: project_id, in: query, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK }

  # ═══════════════════════════════════════════════════════════════
  # Saved Reports
  # ═══════════════════════════════════════════════════════════════

  /v1/saved-reports:
    get:
      summary: Список сохранённых отчётов
      tags: [Dashboards]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: project_id, in: query, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK }
    post:
      summary: Сохранить отчёт
      tags: [Dashboards]
      security: [{ BearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [project_id, name, report_type, config]
              properties:
                project_id: { type: integer }
                name: { type: string }
                report_type: { type: string }
                config: { type: object }
      responses:
        '201': { description: Created }

  /v1/saved-reports/{id}:
    get:
      summary: Получить отчёт
      tags: [Dashboards]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK }
    put:
      summary: Обновить отчёт
      tags: [Dashboards]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK }
    delete:
      summary: Удалить отчёт
      tags: [Dashboards]
      security: [{ BearerAuth: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200':
          content:
            application/json:
              example: { status: "deleted" }

  # ═══════════════════════════════════════════════════════════════
  # Public Query API (Secret Key)
  # ═══════════════════════════════════════════════════════════════

  /v1/api/query/events:
    post:
      summary: Event Segmentation (API)
      description: |
        ```bash
        curl -X POST https://api.trektik.ru/v1/api/query/events \
          -H "X-Secret-Key: sk_abc123..." \
          -d '{"project_id":1,"events":[{"event_name":"page_view"}],"date_range":{"start":"2026-03-01","end":"2026-03-15"},"metric":"total","interval":"day"}'
        ```
      tags: [Management API]
      security: [{ SecretKey: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/QueryRequest' }
      responses:
        '200': { description: OK }
        '401': { description: Unauthorized }
        '429':
          description: Rate limit exceeded
          headers:
            X-RateLimit-Limit: { schema: { type: integer } }
            X-RateLimit-Remaining: { schema: { type: integer } }
            Retry-After: { schema: { type: integer } }

  /v1/api/query/funnels:
    post:
      summary: Funnel Analysis (API)
      tags: [Management API]
      security: [{ SecretKey: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/FunnelRequest' }
      responses:
        '200': { description: OK }
        '401': { description: Unauthorized }

  /v1/api/query/retention:
    post:
      summary: Retention Analysis (API)
      tags: [Management API]
      security: [{ SecretKey: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/RetentionRequest' }
      responses:
        '200': { description: OK }
        '401': { description: Unauthorized }

  /v1/api/query/journeys:
    post:
      summary: User Journeys (API)
      tags: [Management API]
      security: [{ SecretKey: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/JourneyRequest' }
      responses:
        '200': { description: OK }
        '401': { description: Unauthorized }

  /v1/api/query/revenue:
    post:
      summary: Revenue Analysis (API)
      tags: [Management API]
      security: [{ SecretKey: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [project_id, date_range]
              properties:
                project_id: { type: integer }
                date_range:
                  type: object
                  properties:
                    start: { type: string, format: date }
                    end: { type: string, format: date }
      responses:
        '200': { description: OK }

  /v1/api/data-quality:
    get:
      summary: Data Quality (API)
      tags: [Management API]
      security: [{ SecretKey: [] }]
      parameters:
        - { name: project_id, in: query, required: true, schema: { type: integer } }
        - { name: start, in: query, schema: { type: string, format: date } }
        - { name: end, in: query, schema: { type: string, format: date } }
        - { name: limit, in: query, schema: { type: integer, default: 100 } }
        - { name: offset, in: query, schema: { type: integer, default: 0 } }
      responses:
        '200': { description: OK }

  # ── Management API ──────────────────────────────────────────

  /v1/api/projects:
    get:
      summary: List projects (API)
      description: "Scope: read"
      tags: [Management API]
      security: [{ SecretKey: [] }]
      responses:
        '200': { description: OK }
        '403': { description: Insufficient scope }
    post:
      summary: Create project (API)
      description: "Scope: write"
      tags: [Management API]
      security: [{ SecretKey: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string }
                timezone: { type: string, default: "Europe/Moscow" }
      responses:
        '201': { description: Created }
        '403': { description: Insufficient scope }

  /v1/api/projects/{id}:
    get:
      summary: Get project (API)
      tags: [Management API]
      security: [{ SecretKey: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK }
    put:
      summary: Update project (API)
      description: "Scope: write"
      tags: [Management API]
      security: [{ SecretKey: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string }
                timezone: { type: string }
      responses:
        '200': { description: OK }
    delete:
      summary: Delete project (API)
      description: "Scope: admin"
      tags: [Management API]
      security: [{ SecretKey: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK }

  /v1/api/taxonomy/events:
    get:
      summary: List taxonomy events (API)
      tags: [Management API]
      security: [{ SecretKey: [] }]
      parameters:
        - { name: project_id, in: query, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK }
    post:
      summary: Create taxonomy event (API)
      description: "Scope: write"
      tags: [Management API]
      security: [{ SecretKey: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [project_id, event_name]
              properties:
                project_id: { type: integer }
                event_name: { type: string }
                display_name: { type: string }
                category: { type: string }
                description: { type: string }
                status: { type: string, enum: [planned, live, blocked, deprecated] }
      responses:
        '201': { description: Created }

  /v1/api/taxonomy/events/{id}:
    get:
      summary: Get taxonomy event (API)
      tags: [Management API]
      security: [{ SecretKey: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK }
    put:
      summary: Update taxonomy event (API)
      description: "Scope: write"
      tags: [Management API]
      security: [{ SecretKey: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK }
    delete:
      summary: Delete taxonomy event (API)
      description: "Scope: admin"
      tags: [Management API]
      security: [{ SecretKey: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK }

  /v1/api/taxonomy/import:
    post:
      summary: Import taxonomy (API)
      description: "Scope: write"
      tags: [Management API]
      security: [{ SecretKey: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { type: object }
      responses:
        '200': { description: OK }

  /v1/api/taxonomy/export:
    get:
      summary: Export taxonomy (API)
      tags: [Management API]
      security: [{ SecretKey: [] }]
      parameters:
        - { name: project_id, in: query, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK }

  /v1/api/experiments:
    get:
      summary: List experiments (API)
      tags: [Management API]
      security: [{ SecretKey: [] }]
      parameters:
        - { name: project_id, in: query, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK }
    post:
      summary: Create experiment (API)
      description: "Scope: write"
      tags: [Management API]
      security: [{ SecretKey: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [project_id, name, flag_key]
              properties:
                project_id: { type: integer }
                name: { type: string }
                flag_key: { type: string }
                hypothesis: { type: string }
                metric_event: { type: string }
      responses:
        '201': { description: Created }

  /v1/api/experiments/{id}:
    get:
      summary: Get experiment (API)
      tags: [Management API]
      security: [{ SecretKey: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK }
    put:
      summary: Update experiment (API)
      description: "Scope: write"
      tags: [Management API]
      security: [{ SecretKey: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK }
    delete:
      summary: Delete experiment (API)
      description: "Scope: admin"
      tags: [Management API]
      security: [{ SecretKey: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK }

  /v1/api/experiments/{id}/start:
    post:
      summary: Start experiment (API)
      description: "Scope: write"
      tags: [Management API]
      security: [{ SecretKey: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK }

  /v1/api/experiments/{id}/stop:
    post:
      summary: Stop experiment (API)
      description: "Scope: write"
      tags: [Management API]
      security: [{ SecretKey: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK }

  /v1/api/flags:
    get:
      summary: List feature flags (API)
      tags: [Management API]
      security: [{ SecretKey: [] }]
      parameters:
        - { name: project_id, in: query, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK }
    post:
      summary: Create feature flag (API)
      description: "Scope: write"
      tags: [Management API]
      security: [{ SecretKey: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [project_id, key, name]
              properties:
                project_id: { type: integer }
                key: { type: string, example: "new-checkout" }
                name: { type: string, example: "New Checkout Flow" }
                description: { type: string }
                flag_type: { type: string, enum: [boolean, multivariate, percentage] }
      responses:
        '201': { description: Created }

  /v1/api/flags/{id}:
    get:
      summary: Get feature flag (API)
      tags: [Management API]
      security: [{ SecretKey: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK }
    put:
      summary: Update feature flag (API)
      description: "Scope: write"
      tags: [Management API]
      security: [{ SecretKey: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK }
    delete:
      summary: Archive feature flag (API)
      description: "Scope: admin"
      tags: [Management API]
      security: [{ SecretKey: [] }]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200': { description: OK }
