# State Machine Patterns

## Core Concept

Each WhatsApp user has a **session** in the database with a `state` field. The bot routes messages based on this state — a Finite State Machine (FSM).

## Designing States

### Step 1: Map the user journey

```
User sends trigger → Collect needed data → Process with AI → Return result → Done
```

### Step 2: Create one state per "waiting" step

Each state = "the bot is waiting for X from the user"

| State | Bot is waiting for... |
|-------|----------------------|
| `idle` | Trigger command |
| `waiting_api_key` | User's API key |
| `waiting_photo` | Reference image |
| `waiting_description` | Text/audio prompt |
| `generating` | AI processing (internal) |
| `done` | User decides next action |

### Step 3: Define transitions

```
idle ──trigger──→ waiting_X ──input──→ waiting_Y ──input──→ generating ──done──→ done
                                                                                  ↓
                                                                          idle (timeout)
```

## Implementation Pattern

```javascript
export async function handleMessage(message, client) {
  const phone = message.from
  const body = (message.body || '').trim()
  const bodyLower = body.toLowerCase()
  const hasMedia = message.hasMedia
  const msgType = message.type

  // Get or create session
  let session = getSession(phone)
  if (!session) {
    session = createSession(phone, message._data?.notifyName || '')
  }

  // Route by state
  switch (session.state) {
    case 'idle':       return handleIdle(phone, session, message, client)
    case 'waiting_X':  return handleWaitingX(phone, session, message, client)
    case 'waiting_Y':  return handleWaitingY(phone, session, message, client)
    case 'done':       return handleDone(phone, session, message, client)
  }
}
```

## Session Memory (persist across conversations)

Key insight: Save user data in DB so returning users skip completed steps.

```javascript
function handleTrigger(phone, session) {
  // Clear only the current task data, NOT persistent data
  updateSession(phone, { description: null })

  if (session.api_key && session.reference_photo) {
    // Returning user — skip to the action step!
    updateSession(phone, { state: 'waiting_description' })
    send(phone, 'Your data is saved! Just describe what you want.')
  } else if (!session.api_key) {
    updateSession(phone, { state: 'waiting_api_key' })
    send(phone, 'Send your API key first.')
  } else {
    updateSession(phone, { state: 'waiting_photo' })
    send(phone, 'Send your reference photo.')
  }
}
```

## Input Validation Pattern

```javascript
function handleWaitingApiKey(phone, session, message) {
  const body = (message.body || '').trim()

  // Validate format
  if (body.startsWith('AIza') && body.length >= 30 && !body.includes(' ')) {
    updateSession(phone, { api_key: body, state: 'waiting_photo' })
    send(phone, 'Key saved! Now send your photo.')
    return
  }

  // Wrong input type
  if (message.hasMedia) {
    send(phone, 'I need your API key first (text), not a photo.')
    return
  }

  // Cancel
  if (body.toLowerCase() === 'cancelar') {
    updateSession(phone, { state: 'idle' })
    send(phone, 'Cancelled.')
    return
  }

  // Invalid
  send(phone, 'That doesn\'t look like a valid key. It should start with AIza...')
}
```

## Audio Transcription Integration

```javascript
async function handleWaitingDescription(phone, session, message, client) {
  const msgType = message.type
  const body = (message.body || '').trim()

  if (msgType === 'ptt' || msgType === 'audio') {
    // Voice message → transcribe → use as prompt
    const audioMedia = await message.downloadMedia()
    const transcription = await transcribeAudio(
      session.api_key,
      audioMedia.data,
      audioMedia.mimetype
    )
    await startProcessing(phone, session, transcription, client)
    return
  }

  if (body.length > 5) {
    await startProcessing(phone, session, body, client)
    return
  }

  send(phone, 'Describe what you want (text or voice note).')
}
```

## Rate Limiting

```javascript
function checkRateLimit(phone) {
  const maxPerUser = parseInt(getSetting('max_daily_per_user') || '5')
  const maxGlobal = parseInt(getSetting('max_daily_global') || '100')

  const userCount = getUserDailyCount(phone)
  const globalCount = getGlobalDailyCount()

  if (userCount >= maxPerUser) {
    return { limited: true, message: `Daily limit reached (${maxPerUser}). Try again tomorrow.` }
  }
  if (globalCount >= maxGlobal) {
    return { limited: true, message: 'System is busy. Try again later.' }
  }
  return { limited: false }
}
```

## Error Recovery Pattern

```javascript
async function startProcessing(phone, session, prompt, client) {
  const rateCheck = checkRateLimit(phone)
  if (rateCheck.limited) {
    send(phone, rateCheck.message)
    return
  }

  updateSession(phone, { state: 'generating', description: prompt })
  send(phone, 'Processing... please wait.')

  try {
    const results = await doAIWork(session.api_key, prompt, session.reference_data)

    for (const result of results) {
      if (result.success) {
        await sendResult(phone, result, client)
      }
    }

    updateSession(phone, { state: 'done' })
    send(phone, 'Done! Send another prompt or type "nova" to start over.')
  } catch (err) {
    // Recover to a usable state
    if (err.message?.includes('403') || err.message?.includes('API_KEY')) {
      updateSession(phone, { api_key: null, state: 'waiting_api_key' })
      send(phone, 'Your API key is invalid. Please send a new one.')
    } else if (err.message?.includes('429') || err.message?.includes('quota')) {
      updateSession(phone, { state: 'done' })
      send(phone, 'API quota exceeded. Try again later.')
    } else {
      updateSession(phone, { state: 'waiting_description' })
      send(phone, 'Something went wrong. Try sending your request again.')
    }
  }
}
```

## Common User Commands

Always handle these regardless of state:

```javascript
// At the top of handleMessage, before the switch
if (bodyLower === 'cancelar' || bodyLower === 'cancel') {
  updateSession(phone, { state: 'idle' })
  send(phone, 'Cancelled. Send the trigger to start again.')
  return
}

if (bodyLower === 'status') {
  send(phone, `Your state: ${session.state}`)
  return
}

if (bodyLower === 'ajuda' || bodyLower === 'help') {
  send(phone, HELP_MESSAGE)
  return
}
```
