// 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 { expirePairingRequest } from '../../services/pairing.service.js'
import { startSessionTimer, _resetThreeMinFiredForTest, _broadcastTimerResyncForTest } from '../../services/session-timer.service.js'
import { getDb } from '../../db/client.js'
import { PairingFailureCause, SessionStatus } 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_sessions WHERE customer_id = ${id}`
        await sql`DELETE FROM payment_sessions 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_sessions
        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_sessions
      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_sessions
        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_sessions
      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' })
    }
    await expirePairingRequest(target, PairingFailureCause.NO_MITRA_AVAILABLE)
    return { ok: true, session_id: target }
  })

  // 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_sessions 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_sessions (
        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 }
  })
}
