import {
  INBOX_LIMIT,
  pageQueryShape,
  searchTermShape,
  SendFriendRequestSchema,
} from '@crumbs/core/schemas'
import {and, desc, eq} 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 {rethrowUniqueAs} from '../lib/sql'
import {uuidParams, validate} from '../lib/validate'
import {isBlockedBetween} from '../services/blocks'
import {
  friendshipPairWhere,
  hydrateProfileRows,
  listFriendsPage,
} from '../services/friends'
import {requireUserExists} from '../services/users'

const friends = new Hono<{Variables: GuardedVars}>()
  .get(
    '/',
    validate(
      'query',
      z.object({...pageQueryShape, q: searchTermShape.q.optional()}),
    ),
    async (c) => {
      const me = c.get('user')
      const {limit, cursor, q} = c.req.valid('query')
      const {friends, nextCursor} = await listFriendsPage({
        subjectId: me.id,
        viewerId: me.id,
        limit,
        cursor,
        q,
      })
      return c.json({friends, nextCursor})
    },
  )
  .get('/requests', async (c) => {
    const me = c.get('user')
    const rows = await db
      .select({
        id: schema.friendships.id,
        createdAt: schema.friendships.createdAt,
        requesterId: schema.users.id,
        handle: schema.users.handle,
        avatarRef: schema.users.avatarRef,
        userCreatedAt: schema.users.createdAt,
      })
      .from(schema.friendships)
      .innerJoin(
        schema.users,
        eq(schema.users.id, schema.friendships.requesterId),
      )
      .where(
        and(
          eq(schema.friendships.addresseeId, me.id),
          eq(schema.friendships.status, 'pending'),
        ),
      )
      .orderBy(desc(schema.friendships.createdAt))
      .limit(INBOX_LIMIT)

    const profiles = await hydrateProfileRows(
      me.id,
      rows.map((r) => ({
        id: r.requesterId,
        handle: r.handle,
        avatarRef: r.avatarRef,
        createdAt: r.userCreatedAt,
      })),
    )

    const requests = rows.flatMap((r) => {
      const requester = profiles.get(r.requesterId)
      return requester
        ? [
            {
              id: r.id,
              createdAt: r.createdAt.toISOString(),
              requester,
            },
          ]
        : []
    })

    return c.json({requests})
  })
  .post(
    '/requests',
    validate('json', SendFriendRequestSchema),
    async (c) => {
      const me = c.get('user')
      const {addresseeId} = c.req.valid('json')
      if (addresseeId === me.id)
        throw new HTTPException(400, {message: 'cannot_friend_self'})

      await requireUserExists(addresseeId)
      // a block (either direction) forecloses any (re)friending
      if (await isBlockedBetween(me.id, addresseeId))
        throw new HTTPException(403, {message: 'forbidden'})

      const [edge] = await db
        .select()
        .from(schema.friendships)
        .where(friendshipPairWhere(me.id, addresseeId))
        .limit(1)

      if (edge) {
        if (edge.status === 'accepted')
          throw new HTTPException(409, {message: 'already_friends'})
        // pending
        if (edge.addresseeId === me.id) {
          // they already asked us -> accept
          await db
            .update(schema.friendships)
            .set({status: 'accepted', updatedAt: new Date()})
            .where(eq(schema.friendships.id, edge.id))
          return c.json({status: 'friends'})
        }
        throw new HTTPException(409, {message: 'already_requested'})
      }

      await db
        .insert(schema.friendships)
        .values({
          requesterId: me.id,
          addresseeId,
          status: 'pending',
        })
        .catch(rethrowUniqueAs('already_requested'))
      return c.json({status: 'pending_outgoing'})
    },
  )
  .post('/requests/:id/accept', uuidParams('id'), async (c) => {
    const me = c.get('user')
    const {id} = c.req.valid('param')
    firstOr404(
      await db
        .update(schema.friendships)
        .set({status: 'accepted', updatedAt: new Date()})
        .where(
          and(
            eq(schema.friendships.id, id),
            eq(schema.friendships.addresseeId, me.id),
            eq(schema.friendships.status, 'pending'),
          ),
        )
        .returning({id: schema.friendships.id}),
    )
    return c.json({ok: true})
  })
  .post('/requests/:id/decline', uuidParams('id'), async (c) => {
    const me = c.get('user')
    const {id} = c.req.valid('param')
    firstOr404(
      await db
        .delete(schema.friendships)
        .where(
          and(
            eq(schema.friendships.id, id),
            eq(schema.friendships.addresseeId, me.id),
            eq(schema.friendships.status, 'pending'),
          ),
        )
        .returning({id: schema.friendships.id}),
    )
    return c.json({ok: true})
  })
  .delete('/:userId', uuidParams('userId'), async (c) => {
    const me = c.get('user')
    const {userId} = c.req.valid('param')
    firstOr404(
      await db
        .delete(schema.friendships)
        .where(
          and(
            eq(schema.friendships.status, 'accepted'),
            friendshipPairWhere(me.id, userId),
          ),
        )
        .returning({id: schema.friendships.id}),
    )
    return c.json({ok: true})
  })

export default friends
