import {randomBytes} from 'node:crypto'

import {
  INBOX_LIMIT,
  SendTrailInviteSchema,
  SetReactionSchema,
} from '@crumbs/core/schemas'
import {and, desc, eq, isNull, sql} from 'drizzle-orm'
import {Hono} from 'hono'
import {HTTPException} from 'hono/http-exception'
import {z} from 'zod'

import {type GuardedVars} from '../auth/middleware'
import {db, schema} from '../db/client'
import {firstOr404} from '../lib/require'
import {shareLinkUrl, ShareTokenSchema} from '../lib/share-links'
import {publicImageUrl} from '../lib/source-image'
import {countInt, rethrowUniqueAs} from '../lib/sql'
import {uuidParams, validate} from '../lib/validate'
import {areFriends, hydrateProfileRows} from '../services/friends'
import {listCrumbReactors} from '../services/trail-reactions'
import {
  findActiveShareLink,
  requireTrailMember,
  requireTrailViewer,
  trailCoverFallbackRefSql,
} from '../services/trails'
import {requireUserExists} from '../services/users'

// invites, join links, pins, reactions — membership & engagement edges
const trailSocial = new Hono<{Variables: GuardedVars}>()
  .get('/invites', async (c) => {
    const me = c.get('user')
    const rows = await db
      .select({
        id: schema.trailCollaborators.id,
        createdAt: schema.trailCollaborators.createdAt,
        trailId: schema.trails.id,
        trailName: schema.trails.name,
        trailDescription: schema.trails.description,
        coverImageRef: schema.trails.coverImageRef,
        coverFallbackRef: trailCoverFallbackRefSql,
        // preview counts so an invite isn't accepted blind
        crumbCount: sql<number>`(
          select ${countInt()} from trail_crumbs tc
          where tc.trail_id = ${schema.trails.id}
        )`,
        memberCount: sql<number>`(
          select ${countInt()} + 1 from trail_collaborators tc
          where tc.trail_id = ${schema.trails.id}
            and tc.status = 'accepted'
        )`,
        inviterId: schema.users.id,
        inviterHandle: schema.users.handle,
        inviterAvatarRef: schema.users.avatarRef,
        inviterCreatedAt: schema.users.createdAt,
      })
      .from(schema.trailCollaborators)
      .innerJoin(
        schema.trails,
        eq(schema.trails.id, schema.trailCollaborators.trailId),
      )
      .innerJoin(
        schema.users,
        eq(schema.users.id, schema.trailCollaborators.inviterId),
      )
      .where(
        and(
          eq(schema.trailCollaborators.userId, me.id),
          eq(schema.trailCollaborators.status, 'pending'),
        ),
      )
      .orderBy(desc(schema.trailCollaborators.createdAt))
      .limit(INBOX_LIMIT)

    const profiles = await hydrateProfileRows(
      me.id,
      rows.map((r) => ({
        id: r.inviterId,
        handle: r.inviterHandle,
        avatarRef: r.inviterAvatarRef,
        createdAt: r.inviterCreatedAt,
      })),
    )

    const invites = rows.flatMap((r) => {
      const inviter = profiles.get(r.inviterId)
      return inviter
        ? [
            {
              id: r.id,
              createdAt: r.createdAt.toISOString(),
              trail: {
                id: r.trailId,
                name: r.trailName,
                description: r.trailDescription,
                coverImageUrl: publicImageUrl(
                  r.coverImageRef ?? r.coverFallbackRef,
                ),
                crumbCount: r.crumbCount,
                memberCount: r.memberCount,
              },
              inviter,
            },
          ]
        : []
    })

    return c.json({invites})
  })
  .post(
    '/invites/:inviteId/accept',
    uuidParams('inviteId'),
    async (c) => {
      const me = c.get('user')
      const {inviteId} = c.req.valid('param')
      firstOr404(
        await db
          .update(schema.trailCollaborators)
          .set({status: 'accepted', updatedAt: new Date()})
          .where(
            and(
              eq(schema.trailCollaborators.id, inviteId),
              eq(schema.trailCollaborators.userId, me.id),
              eq(schema.trailCollaborators.status, 'pending'),
            ),
          )
          .returning({id: schema.trailCollaborators.id}),
      )
      return c.json({ok: true})
    },
  )
  .post(
    '/invites/:inviteId/decline',
    uuidParams('inviteId'),
    async (c) => {
      const me = c.get('user')
      const {inviteId} = c.req.valid('param')
      firstOr404(
        await db
          .delete(schema.trailCollaborators)
          .where(
            and(
              eq(schema.trailCollaborators.id, inviteId),
              eq(schema.trailCollaborators.userId, me.id),
              eq(schema.trailCollaborators.status, 'pending'),
            ),
          )
          .returning({id: schema.trailCollaborators.id}),
      )
      return c.json({ok: true})
    },
  )
  .post(
    '/join/:token',
    validate('param', z.object({token: ShareTokenSchema})),
    async (c) => {
      const me = c.get('user')
      const {token} = c.req.valid('param')
      const link = await findActiveShareLink(token)
      if (!link) throw new HTTPException(404, {message: 'not_found'})
      const {trail, createdBy} = link

      if (trail.authorId !== me.id) {
        await db
          .insert(schema.trailCollaborators)
          .values({
            trailId: trail.id,
            userId: me.id,
            inviterId: createdBy,
            status: 'accepted',
          })
          .onConflictDoUpdate({
            target: [
              schema.trailCollaborators.trailId,
              schema.trailCollaborators.userId,
            ],
            set: {status: 'accepted', updatedAt: new Date()},
          })
      }
      return c.json({trailId: trail.id})
    },
  )
  .post('/:id/share-link', uuidParams('id'), async (c) => {
    const me = c.get('user')
    const {id} = c.req.valid('param')
    const {trail} = await requireTrailMember(id, me.id)

    const [existing] = await db
      .select({token: schema.trailShareLinks.token})
      .from(schema.trailShareLinks)
      .where(
        and(
          eq(schema.trailShareLinks.trailId, trail.id),
          isNull(schema.trailShareLinks.revokedAt),
        ),
      )
      .limit(1)
    if (existing) return c.json({url: shareLinkUrl(existing.token)})

    const token = randomBytes(24).toString('base64url')
    await db.insert(schema.trailShareLinks).values({
      trailId: trail.id,
      token,
      createdBy: me.id,
    })
    return c.json({url: shareLinkUrl(token)})
  })
  .delete('/:id/share-link', uuidParams('id'), async (c) => {
    const me = c.get('user')
    const {id} = c.req.valid('param')
    const {role} = await requireTrailMember(id, me.id)
    if (role !== 'owner')
      throw new HTTPException(403, {message: 'forbidden'})
    await db
      .update(schema.trailShareLinks)
      .set({revokedAt: new Date()})
      .where(
        and(
          eq(schema.trailShareLinks.trailId, id),
          isNull(schema.trailShareLinks.revokedAt),
        ),
      )
    return c.json({ok: true})
  })
  .put('/:id/pin', uuidParams('id'), async (c) => {
    const me = c.get('user')
    const {id} = c.req.valid('param')
    await requireTrailMember(id, me.id)
    await db
      .insert(schema.trailUserStates)
      .values({trailId: id, userId: me.id, pinnedAt: new Date()})
      .onConflictDoUpdate({
        target: [
          schema.trailUserStates.trailId,
          schema.trailUserStates.userId,
        ],
        set: {pinnedAt: new Date()},
      })
    return c.json({ok: true})
  })
  // unlike PUT, deliberately no membership guard: removed members may
  // still clean up their own state
  .delete('/:id/pin', uuidParams('id'), async (c) => {
    const me = c.get('user')
    const {id} = c.req.valid('param')
    await db
      .update(schema.trailUserStates)
      .set({pinnedAt: null})
      .where(
        and(
          eq(schema.trailUserStates.trailId, id),
          eq(schema.trailUserStates.userId, me.id),
        ),
      )
    return c.json({ok: true})
  })
  .put(
    '/:id/crumbs/:crumbId/reactions',
    uuidParams('id', 'crumbId'),
    validate('json', SetReactionSchema),
    async (c) => {
      const me = c.get('user')
      const {id, crumbId} = c.req.valid('param')
      const {emoji} = c.req.valid('json')
      await requireTrailMember(id, me.id)
      const [inTrail] = await db
        .select({crumbId: schema.trailCrumbs.crumbId})
        .from(schema.trailCrumbs)
        .where(
          and(
            eq(schema.trailCrumbs.trailId, id),
            eq(schema.trailCrumbs.crumbId, crumbId),
          ),
        )
        .limit(1)
      if (!inTrail)
        throw new HTTPException(404, {message: 'not_found'})
      await db
        .insert(schema.crumbReactions)
        .values({trailId: id, crumbId, userId: me.id, emoji})
        .onConflictDoUpdate({
          target: [
            schema.crumbReactions.trailId,
            schema.crumbReactions.crumbId,
            schema.crumbReactions.userId,
          ],
          set: {emoji, createdAt: new Date()},
        })
      return c.json({ok: true})
    },
  )
  // who reacted: trail read access (member or friend-of-author viewer),
  // mirroring the trail detail that already exposes the summaries
  .get(
    '/:id/crumbs/:crumbId/reactions',
    uuidParams('id', 'crumbId'),
    async (c) => {
      const me = c.get('user')
      const {id, crumbId} = c.req.valid('param')
      await requireTrailViewer(id, me.id)
      const reactors = await listCrumbReactors(id, crumbId, me.id)
      return c.json({reactors})
    },
  )
  // unlike PUT, deliberately no membership guard: removed members may
  // still clean up their own state
  .delete(
    '/:id/crumbs/:crumbId/reactions',
    uuidParams('id', 'crumbId'),
    async (c) => {
      const me = c.get('user')
      const {id, crumbId} = c.req.valid('param')
      await db
        .delete(schema.crumbReactions)
        .where(
          and(
            eq(schema.crumbReactions.trailId, id),
            eq(schema.crumbReactions.crumbId, crumbId),
            eq(schema.crumbReactions.userId, me.id),
          ),
        )
      return c.json({ok: true})
    },
  )
  .post(
    '/:id/invites',
    uuidParams('id'),
    validate('json', SendTrailInviteSchema),
    async (c) => {
      const me = c.get('user')
      const {id} = c.req.valid('param')
      const {inviteeId} = c.req.valid('json')

      if (inviteeId === me.id)
        throw new HTTPException(400, {message: 'cannot_invite_self'})

      const {trail} = await requireTrailMember(id, me.id)
      if (inviteeId === trail.authorId)
        throw new HTTPException(409, {message: 'already_member'})

      await requireUserExists(inviteeId)

      if (!(await areFriends(me.id, inviteeId)))
        throw new HTTPException(403, {message: 'not_friends'})

      const [existing] = await db
        .select({status: schema.trailCollaborators.status})
        .from(schema.trailCollaborators)
        .where(
          and(
            eq(schema.trailCollaborators.trailId, trail.id),
            eq(schema.trailCollaborators.userId, inviteeId),
          ),
        )
        .limit(1)
      if (existing)
        throw new HTTPException(409, {
          message:
            existing.status === 'accepted'
              ? 'already_member'
              : 'already_invited',
        })

      await db
        .insert(schema.trailCollaborators)
        .values({
          trailId: trail.id,
          userId: inviteeId,
          inviterId: me.id,
          status: 'pending',
        })
        .catch(rethrowUniqueAs('already_invited'))
      return c.json({ok: true})
    },
  )
  .delete('/:id/collaborators/me', uuidParams('id'), async (c) => {
    const me = c.get('user')
    const {id} = c.req.valid('param')
    firstOr404(
      await db
        .delete(schema.trailCollaborators)
        .where(
          and(
            eq(schema.trailCollaborators.trailId, id),
            eq(schema.trailCollaborators.userId, me.id),
            eq(schema.trailCollaborators.status, 'accepted'),
          ),
        )
        .returning({id: schema.trailCollaborators.id}),
    )
    return c.json({ok: true})
  })

export default trailSocial
