import {eq} from 'drizzle-orm'
import {Hono} from 'hono'
import {HTTPException} from 'hono/http-exception'
import {z} from 'zod'

import {createSession} from '../auth/session'
import {toPublicUser, upsertUserByEmail} from '../auth/users'
import {db, schema} from '../db/client'
import {resetUserData, seedTrail, setHandle} from '../db/dev-seed'
import {setNetworkDown} from '../lib/dev-network'
import {validate} from '../lib/validate'
import {canonicalPair} from '../services/dms'

// test-only login + seeding for mobile e2e. default-disabled: the routes only
// exist when DEV_ROUTES=true is set explicitly (dev/e2e scripts set it), and
// never in production regardless — a deploy that forgets NODE_ENV must not
// ship a login-as-anyone endpoint. auth correctness lives in
// routes/auth.test.ts; this only mints state so e2e flows can skip the
// email + safari + deep-link round trip.

const FRIEND_EMAIL = 'e2e-friend@crumbs.test'

const SeedScenario = z.enum([
  'crumbs',
  'trail',
  'friend',
  'friend-request',
  'trail-invite',
  'dm',
])

const SeedSchema = z.object({
  email: z.email(),
  scenarios: z.array(SeedScenario).default([]),
})

const seedCrumbs = async (authorId: string) => {
  const names = [
    'Blue Bottle Coffee',
    'Tartine Bakery',
    'Dolores Park',
  ]
  const rows = await db
    .insert(schema.crumbs)
    .values(names.map((name) => ({authorId, name})))
    .returning({id: schema.crumbs.id})
  return rows.map((r) => r.id)
}

const dev = new Hono()
  // read the live env so the kill-switch can't be stale and stays testable
  .use('*', async (_c, next) => {
    if (
      process.env.DEV_ROUTES !== 'true' ||
      process.env.NODE_ENV === 'production'
    )
      throw new HTTPException(404, {message: 'not_found'})
    await next()
  })
  .post(
    '/login',
    validate('json', z.object({email: z.email()})),
    async (c) => {
      const {email} = c.req.valid('json')
      const user = await upsertUserByEmail(email)
      const token = await createSession(user.id)
      return c.json({token, user: toPublicUser(user)})
    },
  )
  // flip the api into an unreachable state so an e2e flow can exercise the
  // offline outbox (create → queued → reconnect → synced) without airplane mode
  .post(
    '/network',
    validate('json', z.object({down: z.boolean()})),
    (c) => {
      setNetworkDown(c.req.valid('json').down)
      return c.json({ok: true})
    },
  )
  .post('/seed', validate('json', SeedSchema), async (c) => {
    const {email, scenarios} = c.req.valid('json')
    const user = await upsertUserByEmail(email)
    const needsFriend =
      scenarios.includes('friend') ||
      scenarios.includes('friend-request') ||
      scenarios.includes('trail-invite') ||
      scenarios.includes('dm')
    const friend = needsFriend
      ? await upsertUserByEmail(FRIEND_EMAIL)
      : null

    const userIds = [user.id, ...(friend ? [friend.id] : [])]
    await resetUserData(userIds)
    await setHandle(user.id, 'e2e-user')
    if (friend) await setHandle(friend.id, 'e2e-friend')

    const ids: Record<string, unknown> = {}

    if (scenarios.includes('crumbs') || scenarios.includes('trail')) {
      const crumbIds = await seedCrumbs(user.id)
      ids.crumbIds = crumbIds
      // file only the first crumb so the rest stay addable in add-to-trail
      if (scenarios.includes('trail'))
        ids.trailId = await seedTrail(
          user.id,
          'Weekend plans',
          crumbIds.slice(0, 1),
        )
    }

    if (friend) {
      // accepted friendship with no conversation yet
      if (scenarios.includes('friend'))
        await db.insert(schema.friendships).values({
          requesterId: friend.id,
          addresseeId: user.id,
          status: 'accepted',
        })
      if (scenarios.includes('friend-request'))
        await db.insert(schema.friendships).values({
          requesterId: friend.id,
          addresseeId: user.id,
          status: 'pending',
        })
      if (scenarios.includes('dm')) {
        await db.insert(schema.friendships).values({
          requesterId: friend.id,
          addresseeId: user.id,
          status: 'accepted',
        })
        const [userAId, userBId] = canonicalPair(user.id, friend.id)
        const [conversation] = await db
          .insert(schema.conversations)
          .values({userAId, userBId})
          .returning({id: schema.conversations.id})
        if (!conversation)
          throw new Error('failed to seed conversation')
        ids.conversationId = conversation.id
        const [message] = await db
          .insert(schema.messages)
          .values({
            conversationId: conversation.id,
            senderId: friend.id,
            text: 'Hey, want to grab coffee?',
          })
          .returning({createdAt: schema.messages.createdAt})
        if (message)
          await db
            .update(schema.conversations)
            .set({lastMessageAt: message.createdAt})
            .where(eq(schema.conversations.id, conversation.id))
      }
      if (scenarios.includes('trail-invite')) {
        const inviteTrailId = await seedTrail(
          friend.id,
          'Friend trip',
          [],
        )
        ids.inviteTrailId = inviteTrailId
        await db.insert(schema.trailCollaborators).values({
          trailId: inviteTrailId,
          userId: user.id,
          inviterId: friend.id,
          status: 'pending',
        })
      }
    }

    const token = await createSession(user.id)
    return c.json({token, user: toPublicUser(user), ids})
  })

export default dev
