// Dev/test-only routes. Registration in app.internal.js is gated on
// NODE_ENV !== 'production' so these endpoints never exist in prod builds.
//
// Used by Maestro flows + curl harnesses to read state that the OTP stub
// keeps in memory (the stub-generated code per phone), without baking
// test phone numbers or fixed codes into production code paths.

import { peekStubOtp } from '../../services/otp.service.js'
import { acceptPairingRequest, expirePairingRequest, expireTargetedPairingRequest, getPendingRequestsForMitra } from '../../services/pairing.service.js'
import { startSessionTimer, _resetThreeMinFiredForTest, _broadcastTimerResyncForTest } from '../../services/session-timer.service.js'
import { inspectSessionWsState } from '../../plugins/websocket.js'
import { sendMessage } from '../../services/chat.service.js'
import { sendPushNotification } from '../../services/notification.service.js'
import { getDb } from '../../db/client.js'
import { PairingFailureCause, SessionStatus, UserType } from '../../constants.js'

const sql = getDb()

export const internalTestRoutes = async (fastify) => {
  fastify.get('/peek-otp', async (request, reply) => {
    const phone = request.query?.phone
    if (!phone) {
      return reply.code(400).send({ error: 'phone query param required' })
    }
    const entry = peekStubOtp(phone)
    if (!entry) {
      return reply.code(404).send({ error: 'no_otp_for_phone', phone })
    }
    return entry
  })

  // Wipe rate-limit + cooldown state for a phone so flows can re-run quickly.
  // Deletes otp_requests rows for the phone and (optionally) the customer row
  // so identity-upgrade flows start fresh.
  fastify.post('/reset-phone', async (request, reply) => {
    const phone = request.body?.phone
    if (!phone) {
      return reply.code(400).send({ error: 'phone required in body' })
    }
    const dropCustomer = request.body?.drop_customer === true
    await sql`DELETE FROM otp_requests WHERE phone = ${phone}`
    if (dropCustomer) {
      // Cascade through tables that FK-reference customer.id so re-runs
      // don't trip 23503 from prior test artefacts.
      const ids = await sql`SELECT id FROM customers WHERE phone = ${phone}`
      for (const { id } of ids) {
        await sql`DELETE FROM chat_messages WHERE session_id IN (SELECT id FROM chat_sessions WHERE customer_id = ${id})`
        await sql`DELETE FROM chat_request_notifications WHERE session_id IN (SELECT id FROM chat_sessions WHERE customer_id = ${id})`
        await sql`DELETE FROM customer_transactions WHERE customer_id = ${id}`
        await sql`DELETE FROM chat_sessions WHERE customer_id = ${id}`
        await sql`DELETE FROM payment_requests WHERE customer_id = ${id}`
        await sql`DELETE FROM auth_sessions WHERE user_id = ${id} AND user_type = 'customer'`
      }
      await sql`DELETE FROM customers WHERE phone = ${phone}`
    }
    return { ok: true, phone, dropped_customer: dropCustomer }
  })

  // Force-CONFIRM a `pending` payment session (used by Maestro flows that
  // need to advance past the waiting-payment screen without going through
  // real Xendit). Triggers the same downstream effects (pairing start) that
  // a real webhook would.
  fastify.post('/force-confirm-payment', async (request, reply) => {
    const { latest } = request.body ?? {}
    let target
    if (latest === true) {
      const [row] = await sql`
        SELECT id FROM payment_requests
        WHERE status = 'pending'
        ORDER BY created_at DESC
        LIMIT 1
      `
      if (!row) {
        return reply.code(404).send({ error: 'no_pending_payment' })
      }
      target = row.id
    } else {
      return reply.code(400).send({ error: 'latest:true required in body' })
    }
    const [updated] = await sql`
      UPDATE payment_requests
      SET status = 'confirmed', confirmed_at = NOW()
      WHERE id = ${target} AND status = 'pending'
      RETURNING id, customer_id, status, mode, duration_minutes, is_first_session_discount, targeted_mitra_id
    `
    if (!updated) {
      return reply.code(409).send({ error: 'payment_state_changed' })
    }
    // Customer-app waiting screen polls and will advance once it sees
    // `confirmed`. Pairing creation is the customer-app's responsibility
    // (createPairingRequest is called from the searching screen).
    return { ok: true, payment: updated }
  })

  // Force-expire a `pending` payment session (used by Maestro Stage 3 flow to
  // drive the waiting-payment screen into the expired state without waiting
  // 20 minutes). Sets `expires_at` to the past and status to `expired` so the
  // next poll from the client sees the terminal state.
  //
  // Body shape:
  //   { payment_id: '<uuid>' }      → expire this specific session
  //   { latest: true }              → expire the most-recently-created pending
  fastify.post('/force-expire-payment', async (request, reply) => {
    const { payment_id, latest } = request.body ?? {}
    let target
    if (latest === true) {
      const [row] = await sql`
        SELECT id FROM payment_requests
        WHERE status = 'pending'
        ORDER BY created_at DESC
        LIMIT 1
      `
      if (!row) {
        return reply.code(404).send({ error: 'no_pending_payment' })
      }
      target = row.id
    } else if (payment_id) {
      target = payment_id
    } else {
      return reply.code(400).send({ error: 'payment_id or latest:true required in body' })
    }
    const [updated] = await sql`
      UPDATE payment_requests
      SET status = 'expired', expires_at = NOW() - INTERVAL '1 minute'
      WHERE id = ${target} AND status = 'pending'
      RETURNING id, status
    `
    if (!updated) {
      return reply.code(404).send({ error: 'no_pending_payment_for_id', payment_id: target })
    }
    return { ok: true, ...updated }
  })

  // Force-expire a pairing blast (used by Maestro Stage 5 flow to drive the
  // searching screen into the timeout state without waiting 5 minutes). Marks
  // the most-recently-created blast chat_session as no_mitra_available.
  //
  // Body shape:
  //   { session_id: '<uuid>' }   → expire this specific session
  //   { latest: true }           → expire the most-recent SEARCHING session
  fastify.post('/force-pairing-timeout', async (request, reply) => {
    const { session_id, latest } = request.body ?? {}
    let target = session_id
    if (latest === true) {
      // Accept either `searching` (general blast in flight) or
      // `pending_acceptance` (a mitra has been offered but hasn't accepted)
      // — both are pre-paired states the 5-min timer would terminate.
      const [row] = await sql`
        SELECT id FROM chat_sessions
        WHERE status IN (${SessionStatus.SEARCHING}, ${SessionStatus.PENDING_ACCEPTANCE})
        ORDER BY created_at DESC
        LIMIT 1
      `
      if (!row) {
        return reply.code(404).send({ error: 'no_searching_session' })
      }
      target = row.id
    }
    if (!target) {
      return reply.code(400).send({ error: 'session_id or latest:true required in body' })
    }
    // Branch targeted vs blast: a chat_session linked to a payment with
    // `targeted_mitra_id` is a TARGETED pair waiting for that specific mitra
    // to accept (20s countdown). Its expiry must fire RETURNING_CHAT_TIMEOUT
    // — which the customer-side TargetedWaitingScreen listens for to surface
    // the post-pay BestieOfflinePopup (returning variant). Blast pairs go
    // through the regular PAIRING_FAILED → S7 timeout screen path.
    const [linked] = await sql`
      SELECT ps.targeted_mitra_id
      FROM chat_sessions cs
      LEFT JOIN payment_requests ps ON ps.id = cs.payment_request_id
      WHERE cs.id = ${target}
      LIMIT 1
    `
    if (linked?.targeted_mitra_id) {
      await expireTargetedPairingRequest(target)
      return { ok: true, session_id: target, kind: 'targeted' }
    }
    await expirePairingRequest(target, PairingFailureCause.NO_MITRA_AVAILABLE)
    return { ok: true, session_id: target, kind: 'blast' }
  })

  // Force-set the expires_at of an active chat_session to drive Phase 4
  // Stage 6 countdown UX (3-min snackbar, last-2-min danger, expired banner)
  // without waiting in real time. Reschedules the in-memory session timer so
  // `session_warning` / `session_timer` / `session_expired` WS events fire on
  // the new schedule.
  //
  // Body shape:
  //   { seconds_from_now: 175 }                  → expire latest active session in N seconds
  //   { session_id: '<uuid>', seconds_from_now } → expire specific session
  fastify.post('/force-session-expires-at', async (request, reply) => {
    const { session_id, seconds_from_now } = request.body ?? {}
    if (typeof seconds_from_now !== 'number') {
      return reply.code(400).send({ error: 'seconds_from_now (number) required' })
    }
    let target = session_id
    if (!target) {
      const [row] = await sql`
        SELECT id FROM chat_sessions
        WHERE status = ${SessionStatus.ACTIVE}
        ORDER BY created_at DESC
        LIMIT 1
      `
      if (!row) {
        return reply.code(404).send({ error: 'no_active_session' })
      }
      target = row.id
    }
    const [updated] = await sql`
      UPDATE chat_sessions
      SET expires_at = NOW() + (${seconds_from_now} || ' seconds')::interval
      WHERE id = ${target} AND status = ${SessionStatus.ACTIVE}
      RETURNING id, expires_at
    `
    if (!updated) {
      return reply.code(404).send({ error: 'no_active_session_for_id', session_id: target })
    }
    // Allow the 3-min warning to fire again on the new schedule.
    _resetThreeMinFiredForTest(updated.id)
    startSessionTimer(updated.id, updated.expires_at)
    // Push an immediate WS resync so the customer UI's local ticker tracks
    // the new schedule without waiting for the next scheduled event.
    _broadcastTimerResyncForTest(updated.id, updated.expires_at)
    return { ok: true, session_id: updated.id, expires_at: updated.expires_at }
  })

  // Seed a completed chat_sessions row for the customer linked to `phone`,
  // pairing them with the most-recent online mitra. Used by Maestro Stage 8
  // flow (08_returning_targeted.yaml) so the bestie history list isn't empty.
  //
  // Body shape:
  //   { phone: '+62...' } — the customer; mitra is auto-picked.
  fastify.post('/seed-history-session', async (request, reply) => {
    const phone = request.body?.phone
    if (!phone) {
      return reply.code(400).send({ error: 'phone required in body' })
    }
    const [customer] = await sql`
      SELECT id FROM customers WHERE phone = ${phone} LIMIT 1
    `
    if (!customer) {
      return reply.code(404).send({ error: 'no_customer_for_phone', phone })
    }
    const [mitra] = await sql`
      SELECT m.id, m.display_name FROM mitras m
      INNER JOIN mitra_online_status s ON s.mitra_id = m.id
      WHERE s.is_online = true
      ORDER BY s.last_heartbeat_at DESC NULLS LAST
      LIMIT 1
    `
    if (!mitra) {
      return reply.code(404).send({ error: 'no_online_mitra' })
    }
    const [session] = await sql`
      INSERT INTO chat_sessions (
        customer_id, mitra_id, status, topic_sensitivity, topics,
        created_at, paired_at, ended_at, duration_minutes, price
      ) VALUES (
        ${customer.id}, ${mitra.id}, ${SessionStatus.COMPLETED}, 'regular',
        ${sql.array(['hubungan'])},
        NOW() - INTERVAL '1 day', NOW() - INTERVAL '1 day',
        NOW() - INTERVAL '1 day' + INTERVAL '15 minutes',
        15, 30000
      )
      RETURNING id
    `
    return { ok: true, session_id: session.id, mitra_id: mitra.id, mitra_name: mitra.display_name }
  })

  // Seed a payment_requests row in `pending` status for the customer linked
  // to `phone`, with expires_at safely in the future. Used by Maestro Stage
  // 10 flow (09_chat_tab.yaml) to populate the Pembayaran sub-tab without
  // walking the multi-screen S6 paywall → method → duration → method flow.
  //
  // Body: { phone, isExtension?, amount?, durationMinutes?, mode? }
  //   - isExtension: defaults false (initial-session payment)
  //   - amount: defaults 5000 IDR
  //   - durationMinutes: defaults 15
  //   - mode: 'chat' (default) | 'call'
  fastify.post('/seed-pending-payment', async (request, reply) => {
    const phone = request.body?.phone
    if (!phone) {
      return reply.code(400).send({ error: 'phone required in body' })
    }
    const isExtension = request.body?.isExtension === true
    const amount = Number.isFinite(request.body?.amount) ? request.body.amount : 5000
    const durationMinutes = Number.isFinite(request.body?.durationMinutes)
      ? request.body.durationMinutes
      : 15
    const mode = request.body?.mode === 'call' ? 'call' : 'chat'

    const [customer] = await sql`
      SELECT id FROM customers WHERE phone = ${phone} LIMIT 1
    `
    if (!customer) {
      return reply.code(404).send({ error: 'no_customer_for_phone', phone })
    }
    const [row] = await sql`
      INSERT INTO payment_requests (
        customer_id, amount, duration_minutes, is_first_session_discount,
        is_extension, status, mode, expires_at
      ) VALUES (
        ${customer.id}, ${amount}, ${durationMinutes}, false,
        ${isExtension}, 'pending', ${mode}, NOW() + INTERVAL '20 minutes'
      )
      RETURNING id, customer_id, amount, is_extension, status, expires_at
    `
    return { ok: true, payment_id: row.id, ...row }
  })

  // Upsert a customer row with phone + display_name (is_anonymous=false).
  // Used by Maestro TS-07 to set up the "returning user already has a name"
  // precondition: a real returning OTP sign-in must skip the set-name screen
  // because resolveCustomerForIdentity returns the existing row unchanged.
  //
  // Body: { phone, display_name }
  fastify.post('/seed-customer', async (request, reply) => {
    const phone = request.body?.phone
    const display_name = request.body?.display_name
    if (!phone || !display_name) {
      return reply.code(400).send({ error: 'phone and display_name required in body' })
    }
    const [row] = await sql`
      INSERT INTO customers (phone, display_name, is_anonymous)
      VALUES (${phone}, ${display_name}, false)
      ON CONFLICT (phone) DO UPDATE
        SET display_name = EXCLUDED.display_name,
            is_anonymous = false
      RETURNING id, phone, display_name, is_anonymous
    `
    return { ok: true, ...row }
  })

  // Upsert a mitra with the given phone and is_active flag. Used by the
  // mitra_app pre-home Maestro flows to ensure a known mitra exists (with
  // the right active state) before driving the OTP verify path. Idempotent —
  // safe to run on every test pass.
  fastify.post('/seed-mitra', async (request, reply) => {
    const phone = request.body?.phone
    const display_name = request.body?.display_name
    const is_active = request.body?.is_active !== false // default true
    if (!phone || !display_name) {
      return reply.code(400).send({ error: 'phone and display_name required in body' })
    }
    const [row] = await sql`
      INSERT INTO mitras (phone, display_name, is_active)
      VALUES (${phone}, ${display_name}, ${is_active})
      ON CONFLICT (phone) DO UPDATE
        SET display_name = EXCLUDED.display_name,
            is_active = EXCLUDED.is_active
      RETURNING id, phone, display_name, is_active
    `
    return { ok: true, ...row }
  })

  // Mark EVERY mitra row online. Used by Maestro flows as a setup step to
  // ensure a clean known-good state regardless of what previous tests did
  // (e.g. force-mitra-offline leaving the dev DB with no online mitras).
  // Inserts a status row for every mitra that doesn't have one yet.
  fastify.post('/reset-all-mitras-online', async (_request, reply) => {
    await sql`
      INSERT INTO mitra_online_status
        (mitra_id, is_online, last_online_at, last_heartbeat_at)
      SELECT id, true, NOW(), NOW() FROM mitras
      ON CONFLICT (mitra_id) DO UPDATE SET
        is_online = true,
        last_online_at = NOW(),
        last_heartbeat_at = NOW()
    `
    const [count] = await sql`
      SELECT COUNT(*)::int AS n FROM mitra_online_status WHERE is_online = true
    `
    return { ok: true, online_count: count.n }
  })

  // Force a mitra ONLINE in mitra_online_status — used by Maestro flows that
  // need a SECOND online mitra (TS-02, TS-06) when the dev DB only has one
  // signed-in mitra. With `exclude_mitra_id`, picks any other mitra (offline
  // or never-online) and upserts their status row as online. Without it,
  // either targets the explicit `mitra_id` or the first offline candidate.
  //
  // Body: { mitra_id?: uuid, exclude_mitra_id?: uuid }
  fastify.post('/force-mitra-online', async (request, reply) => {
    const { mitra_id, exclude_mitra_id } = request.body ?? {}
    let target = mitra_id
    if (!target) {
      // Pick any mitra other than exclude — already-online is fine (the
      // UPSERT below is idempotent). The intent is "ensure a different
      // mitra IS online", not "force a state change".
      const [row] = await sql`
        SELECT m.id, m.display_name FROM mitras m
        WHERE (${exclude_mitra_id ?? null}::uuid IS NULL
               OR m.id != ${exclude_mitra_id ?? null}::uuid)
        ORDER BY m.id
        LIMIT 1
      `
      if (!row) {
        return reply.code(404).send({
          error: 'no_other_mitra_available',
          exclude_mitra_id: exclude_mitra_id ?? null,
        })
      }
      target = row.id
    }
    const [updated] = await sql`
      INSERT INTO mitra_online_status
        (mitra_id, is_online, last_online_at, last_heartbeat_at)
      VALUES (${target}, true, NOW(), NOW())
      ON CONFLICT (mitra_id) DO UPDATE SET
        is_online = true,
        last_online_at = NOW(),
        last_heartbeat_at = NOW()
      RETURNING mitra_id, is_online, last_heartbeat_at
    `
    return { ok: true, ...updated }
  })

  // Force a specific mitra OFFLINE in mitra_online_status — used by Maestro
  // flows (TS-02 / TS-03 in requirement/phase4-customer-flow.md) that need the
  // customer's history-list bestie row to render in its offline (dimmed) state.
  // Distinct from the CC `mitra-online-status/:id/offline` endpoint which
  // requires a CC_JWT; this one is unauthenticated (NODE_ENV-gated) so flows
  // don't need CC credentials.
  //
  // Body: { mitra_id }
  fastify.post('/force-mitra-offline', async (request, reply) => {
    const mitraId = request.body?.mitra_id
    if (!mitraId) {
      return reply.code(400).send({ error: 'mitra_id required in body' })
    }
    const [updated] = await sql`
      UPDATE mitra_online_status
      SET is_online = false,
          last_heartbeat_at = NOW() - INTERVAL '10 minutes'
      WHERE mitra_id = ${mitraId}
      RETURNING mitra_id, is_online, last_heartbeat_at
    `
    if (!updated) {
      return reply.code(404).send({ error: 'no_online_status_row', mitra_id: mitraId })
    }
    return { ok: true, ...updated }
  })

  // Delete the mitra_online_status row for a given mitra — used by Maestro
  // scenario flows that need to simulate a "freshly created mitra with NO
  // status row yet" (the natural state right after seed_mitra and before
  // any /api/mitra/status call from the app). The app's first /status call
  // re-creates the row via ensureStatusRow() with the DB default
  // is_online=false; this endpoint just rewinds to that pre-state.
  //
  // Body: { mitra_id }
  fastify.post('/delete-mitra-status-row', async (request, reply) => {
    const mitraId = request.body?.mitra_id
    if (!mitraId) {
      return reply.code(400).send({ error: 'mitra_id required in body' })
    }
    const result = await sql`
      DELETE FROM mitra_online_status WHERE mitra_id = ${mitraId}
      RETURNING mitra_id
    `
    return { ok: true, mitra_id: mitraId, deleted: result.length > 0 }
  })

  // Accept the most recent pending pairing notification, regardless of which
  // mitra it was sent to. Used by Maestro flows where the test doesn't know
  // (or care) which specific mitra should accept — e.g. TS-02 (blast where
  // the seeded mitra was forced offline, so an unknown OTHER online mitra
  // got the notification). No body required.
  fastify.post('/accept-latest-pending', async (_request, reply) => {
    const [notif] = await sql`
      SELECT n.session_id, n.mitra_id
      FROM chat_request_notifications n
      JOIN chat_sessions s ON s.id = n.session_id
      WHERE s.status IN (${SessionStatus.SEARCHING}, ${SessionStatus.PENDING_ACCEPTANCE})
        AND n.response IS NULL
      ORDER BY n.notified_at DESC
      LIMIT 1
    `
    if (!notif) {
      return reply.code(404).send({ error: 'no_pending_notification' })
    }
    const session = await acceptPairingRequest(notif.session_id, notif.mitra_id)
    return { ok: true, session_id: notif.session_id, mitra_id: notif.mitra_id, session }
  })

  // Accept the most recent pending pairing request for a given mitra without
  // needing a mitra JWT. Used by Maestro flows that drive the customer side
  // through to the post-payment waiting screen and need the mitra side to
  // "accept" so the customer transitions onward (see TS-01 in
  // requirement/phase4-customer-flow.md).
  //
  // Body: { mitra_id }
  fastify.post('/mitra-accept-latest', async (request, reply) => {
    const mitraId = request.body?.mitra_id
    if (!mitraId) {
      return reply.code(400).send({ error: 'mitra_id required in body' })
    }
    const pending = await getPendingRequestsForMitra(mitraId)
    if (!pending || pending.length === 0) {
      return reply.code(404).send({ error: 'no_pending_request', mitra_id: mitraId })
    }
    // Newest first — flows always want the request that was just created.
    const latest = pending[pending.length - 1]
    const session = await acceptPairingRequest(latest.session_id, mitraId)
    return { ok: true, session_id: latest.session_id, session }
  })

  // Test-only: read the in-memory websocket connection state for a session.
  // Used by Maestro flows asserting that backgrounding the customer/mitra
  // app closed its WebSocket (which is what gates FCM fallback per
  // chat.service.js:51). Returns booleans per role.
  fastify.get('/ws-connection-state', async (request, reply) => {
    const sessionId = request.query?.session_id
    if (!sessionId) {
      return reply.code(400).send({ error: 'session_id query param required' })
    }
    return inspectSessionWsState(sessionId)
  })

  // Test-only: emulate an FCM push to a specific customer without going
  // through the chat-session/WS-fallback path. Useful for poking the
  // device's local-notification handler in isolation (e.g. verifying the
  // `chat_messages` channel renders, FCM token validity, etc).
  //
  // Body: { customer_id?, latest_customer?, content? }
  //   - customer_id: target a specific customers.id.
  //   - latest_customer: true → pick the most-recently-created customer
  //     that has an fcm_token (handy when the maestro flow just signed in
  //     anonymously and you don't have the UUID).
  //   - content: optional override for the notification body text.
  fastify.post('/send-fcm-chat-message', async (request, reply) => {
    const { customer_id: customerId, latest_customer: latest, content } =
      request.body ?? {}
    let targetId = customerId
    if (!targetId && latest === true) {
      const [row] = await sql`
        SELECT id FROM customers
        WHERE fcm_token IS NOT NULL
        ORDER BY created_at DESC
        LIMIT 1
      `
      if (!row) {
        return reply.code(404).send({ error: 'no_customer_with_fcm_token' })
      }
      targetId = row.id
    }
    if (!targetId) {
      return reply.code(400).send({
        error: 'customer_id or latest_customer:true required in body',
      })
    }
    const body = content || 'Pesan baru dari bestie · ketuk buat balas'
    const ok = await sendPushNotification(UserType.CUSTOMER, targetId, {
      title: 'Pesan baru dari Bestie',
      body,
      data: { type: 'chat_message', session_id: 'test-emulated' },
    })
    return { ok, customer_id: targetId, body }
  })

  // Test-only: send a chat message AS the customer of a paired session.
  // Mirrors send-chat-message-as-mitra but with senderType=CUSTOMER —
  // useful for asserting the mitra-side FCM fallback when the mitra app is
  // backgrounded. Returns the dispatch transport so the caller can assert
  // delivered_via=fcm.
  //
  // Body: { session_id, content }
  fastify.post('/send-chat-message-as-customer', async (request, reply) => {
    const { session_id: sessionId, content } = request.body ?? {}
    if (!sessionId || !content) {
      return reply.code(400).send({ error: 'session_id and content required in body' })
    }
    const [session] = await sql`
      SELECT customer_id FROM chat_sessions WHERE id = ${sessionId} LIMIT 1
    `
    if (!session?.customer_id) {
      return reply.code(404).send({ error: 'no_session_or_customer', session_id: sessionId })
    }
    // Recipient here is the mitra — inspect its WS state before dispatch so
    // we can answer "websocket" vs "fcm" honestly.
    const wsBefore = inspectSessionWsState(sessionId)
    const message = await sendMessage({
      sessionId,
      senderType: UserType.CUSTOMER,
      senderId: session.customer_id,
      content,
    })
    return {
      ok: true,
      message_id: message.id,
      delivered_via: wsBefore.mitra_connected ? 'websocket' : 'fcm',
    }
  })

  // Test-only: send a chat message AS the mitra of a paired session, using
  // the real chat.service.sendMessage code path. Returns which transport
  // actually carried the message — useful for asserting the WS-vs-FCM
  // fallback (e.g. Maestro backgrounds the customer app, calls this, and
  // expects `delivered_via: "fcm"`).
  //
  // Body: { session_id, content }
  fastify.post('/send-chat-message-as-mitra', async (request, reply) => {
    const { session_id: sessionId, content } = request.body ?? {}
    if (!sessionId || !content) {
      return reply.code(400).send({ error: 'session_id and content required in body' })
    }
    const [session] = await sql`
      SELECT mitra_id FROM chat_sessions WHERE id = ${sessionId} LIMIT 1
    `
    if (!session?.mitra_id) {
      return reply.code(404).send({ error: 'no_session_or_mitra', session_id: sessionId })
    }
    // Snapshot WS state BEFORE the send so we can answer "which path?"
    // honestly: sendMessage tries WS first, falls back to FCM only when WS
    // returned false. We inspect customer_connected here because the mitra
    // is the sender — recipient is the customer.
    const wsBefore = inspectSessionWsState(sessionId)
    const message = await sendMessage({
      sessionId,
      senderType: UserType.MITRA,
      senderId: session.mitra_id,
      content,
    })
    return {
      ok: true,
      message_id: message.id,
      delivered_via: wsBefore.customer_connected ? 'websocket' : 'fcm',
    }
  })
}
