import {resolveMentionFacets} from '@crumbs/core/mentions'
import {
  CreateTrailPostSchema,
  pageQueryShape,
} from '@crumbs/core/schemas'
import {truncate} from '@crumbs/core/text'
import {and, eq} from 'drizzle-orm'
import {Hono} from 'hono'
import {z} from 'zod'

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 {firstOr404} from '../lib/require'
import {uuidParams, validate} from '../lib/validate'
import {notifyTrailMembers} from '../services/trail-notifications'
import {listTrailPosts, toTrailPost} from '../services/trail-posts'
import {
  listTrailMembers,
  requireOwnerOrRowAuthor,
  requireTrailMember,
  requireTrailViewer,
  touchTrailView,
} from '../services/trails'

// collapse generic new-post pushes into one per trail per window;
// personal mention pushes are never throttled
const POST_PUSH_THROTTLE_SECONDS = 5 * 60

const notifyPost = async (
  trail: {id: string; name: string; authorId: string},
  actor: {id: string; handle: string},
  text: string,
  mentionedIds: string[],
  members: {id: string}[],
) => {
  const preview = truncate(text, PUSH_PREVIEW_MAX)
  const mentioned = mentionedIds.filter((id) => id !== actor.id)
  await Promise.all([
    sendPushToUsers(mentioned, (token) => ({
      to: token,
      title: trail.name,
      body: `@${actor.handle} mentioned you: ${preview}`,
      data: {trailId: trail.id},
    })),
    notifyTrailMembers({
      trail,
      actorId: actor.id,
      body: `@${actor.handle}: ${preview}`,
      throttle: {
        key: `trail-post-push:${trail.id}`,
        ttlSeconds: POST_PUSH_THROTTLE_SECONDS,
      },
      excludeUserIds: mentioned,
      members,
    }),
  ])
}

// member-authored text posts on a trail
const trailPosts = new Hono<{Variables: GuardedVars}>()
  .post(
    '/:id/posts',
    uuidParams('id'),
    validate('json', CreateTrailPostSchema),
    async (c) => {
      const user = c.get('user')
      const {id} = c.req.valid('param')
      const {id: postId, text} = c.req.valid('json')

      const {trail} = await requireTrailMember(id, user.id)
      const members = await listTrailMembers(trail)
      const facets = resolveMentionFacets(text, members)

      // a client-minted id makes the create idempotent under replay
      const [post] = await db
        .insert(schema.trailPosts)
        .values({
          ...(postId ? {id: postId} : {}),
          trailId: trail.id,
          authorId: user.id,
          text,
          facets,
        })
        .onConflictDoNothing({target: schema.trailPosts.id})
        .returning()

      // the client id already exists: an idempotent replay of this author's
      // post (its side effects landed on the first create — return it), or a
      // forged id we must reject
      if (!post) {
        const existing = await resolveClientIdReplay(
          postId,
          async (pid) => {
            const [row] = await db
              .select()
              .from(schema.trailPosts)
              .where(eq(schema.trailPosts.id, pid))
              .limit(1)
            return row
          },
          (row) =>
            row.authorId === user.id && row.trailId === trail.id,
        )
        return c.json({
          post: toTrailPost({
            post: existing,
            authorHandle: user.handle,
            authorAvatarRef: user.avatarRef,
          }),
        })
      }

      const mentionedIds = [...new Set(facets.map((f) => f.userId))]
      Promise.all([
        touchTrailView(trail.id, user.id),
        db
          .update(schema.trails)
          .set({updatedAt: new Date()})
          .where(eq(schema.trails.id, trail.id)),
        notifyPost(
          trail,
          {id: user.id, handle: user.handle},
          post.text,
          mentionedIds,
          members,
        ),
      ]).catch((err: unknown) =>
        console.error('trail post side effects failed', {
          trailId: trail.id,
          err,
        }),
      )

      return c.json({
        post: toTrailPost({
          post,
          authorHandle: user.handle,
          authorAvatarRef: user.avatarRef,
        }),
      })
    },
  )
  .get(
    '/:id/posts',
    uuidParams('id'),
    validate('query', z.object(pageQueryShape)),
    async (c) => {
      const user = c.get('user')
      const {id} = c.req.valid('param')
      const {limit, cursor} = c.req.valid('query')

      // same visibility as trail detail: members, else friend-viewable
      await requireTrailViewer(id, user.id)

      const {posts, nextCursor} = await listTrailPosts(id, {
        viewerId: user.id,
        limit,
        cursor,
      })
      return c.json({posts, nextCursor})
    },
  )
  .delete(
    '/:id/posts/:postId',
    uuidParams('id', 'postId'),
    async (c) => {
      const user = c.get('user')
      const {id, postId} = c.req.valid('param')
      const {role} = await requireTrailMember(id, user.id)

      await requireOwnerOrRowAuthor(role, user.id, async () => {
        const [post] = await db
          .select({authorId: schema.trailPosts.authorId})
          .from(schema.trailPosts)
          .where(eq(schema.trailPosts.id, postId))
          .limit(1)
        return post?.authorId
      })

      firstOr404(
        await db
          .delete(schema.trailPosts)
          .where(
            and(
              eq(schema.trailPosts.id, postId),
              eq(schema.trailPosts.trailId, id),
            ),
          )
          .returning({id: schema.trailPosts.id}),
      )
      return c.json({ok: true})
    },
  )

export default trailPosts
