import {
  CreateConversationSchema,
  ListDmMessagesQuerySchema,
  SendDmSchema,
} from '@crumbs/core/schemas'
import {truncate} from '@crumbs/core/text'
import {eq} from 'drizzle-orm'
import {Hono} from 'hono'
import {HTTPException} from 'hono/http-exception'

import {type GuardedVars} from '../auth/middleware'
import {db, schema} from '../db/client'
import {resolveClientIdReplay} from '../lib/idempotency'
import {PUSH_PREVIEW_MAX, sendPushToUsers} from '../lib/push'
import {uuidParams, validate} from '../lib/validate'
import {
  getConversationView,
  getOrCreateConversation,
  listConversations,
  listMessages,
  listMessagesSince,
  markRead,
  peerIdFor,
  requireConversationMember,
  toDmMessage,
} from '../services/dms'
import {areFriends} from '../services/friends'
import {requireUserExists} from '../services/users'

// 1:1 direct messages between friends; plain text, no mentions
const dms = new Hono<{Variables: GuardedVars}>()
  .get('/', async (c) => {
    const user = c.get('user')
    return c.json({conversations: await listConversations(user.id)})
  })
  .post(
    '/',
    validate('json', CreateConversationSchema),
    async (c) => {
      const user = c.get('user')
      const {userId} = c.req.valid('json')

      if (userId === user.id)
        throw new HTTPException(400, {message: 'cannot_dm_self'})
      await requireUserExists(userId)
      if (!(await areFriends(user.id, userId)))
        throw new HTTPException(403, {message: 'not_friends'})

      const conversation = await getOrCreateConversation(
        user.id,
        userId,
      )
      return c.json({
        conversation: await getConversationView(
          user.id,
          conversation.id,
        ),
      })
    },
  )
  .get(
    '/:id/messages',
    uuidParams('id'),
    validate('query', ListDmMessagesQuerySchema),
    async (c) => {
      const user = c.get('user')
      const {id} = c.req.valid('param')
      const {limit, cursor, since} = c.req.valid('query')

      await requireConversationMember(id, user.id)

      if (since)
        return c.json({
          messages: await listMessagesSince(id, new Date(since)),
          nextCursor: null,
        })
      const {messages, nextCursor} = await listMessages(id, {
        limit,
        cursor,
      })
      return c.json({messages, nextCursor})
    },
  )
  .post(
    '/:id/messages',
    uuidParams('id'),
    validate('json', SendDmSchema),
    async (c) => {
      const user = c.get('user')
      const {id} = c.req.valid('param')
      const {id: messageId, text} = c.req.valid('json')

      const {conversation, side} = await requireConversationMember(
        id,
        user.id,
      )
      const peerId = peerIdFor(conversation, user.id)
      // unfriending closes the channel
      if (!(await areFriends(user.id, peerId)))
        throw new HTTPException(403, {message: 'not_friends'})

      // a client-minted id makes the send idempotent under replay. one
      // transaction with the conversation bump: a crash between them would
      // otherwise leave the message in but the inbox ordering stale forever,
      // since the replay's conflict branch intentionally skips the bump
      const message = await db.transaction(async (tx) => {
        const [inserted] = await tx
          .insert(schema.messages)
          .values({
            ...(messageId ? {id: messageId} : {}),
            conversationId: conversation.id,
            senderId: user.id,
            text,
          })
          .onConflictDoNothing({target: schema.messages.id})
          .returning()
        if (!inserted) return null
        // sending implies having read everything before it
        await tx
          .update(schema.conversations)
          .set({
            lastMessageAt: inserted.createdAt,
            ...(side === 'a'
              ? {userALastReadAt: inserted.createdAt}
              : {userBLastReadAt: inserted.createdAt}),
          })
          .where(eq(schema.conversations.id, conversation.id))
        return inserted
      })

      // the client id already exists: an idempotent replay of this sender's
      // message (its read-bump and push landed on the first send — return it),
      // or a forged id we must reject, never leaking another message
      if (!message) {
        const existing = await resolveClientIdReplay(
          messageId,
          async (mid) => {
            const [row] = await db
              .select()
              .from(schema.messages)
              .where(eq(schema.messages.id, mid))
              .limit(1)
            return row
          },
          (row) =>
            row.senderId === user.id &&
            row.conversationId === conversation.id,
        )
        return c.json({message: toDmMessage(existing)})
      }

      sendPushToUsers([peerId], (token) => ({
        to: token,
        title: `@${user.handle}`,
        body: truncate(message.text, PUSH_PREVIEW_MAX),
        data: {type: 'dm', conversationId: conversation.id},
      })).catch((err: unknown) =>
        console.error('dm push failed', {
          conversationId: conversation.id,
          err,
        }),
      )

      return c.json({message: toDmMessage(message)})
    },
  )
  .put('/:id/read', uuidParams('id'), async (c) => {
    const user = c.get('user')
    const {id} = c.req.valid('param')
    const {conversation, side} = await requireConversationMember(
      id,
      user.id,
    )
    await markRead(conversation.id, side)
    return c.json({ok: true})
  })

export default dms
